Python Development Environment: Package Managers, Type Checking, and Linting
Python Development Environment: Package Managers, Type Checking, and Linting
Python's development tooling has historically been fragmented and confusing. The good news: in 2026, the ecosystem has consolidated around better tools. Here's how to set up a Python development environment that doesn't fight you.
Package Managers: uv Has Won
The Python package management landscape has gone through pip, pipenv, poetry, pdm, and now uv. Here's where things stand.
| Tool | Speed | Lock file | Resolver | Virtual envs | Recommended? |
|---|---|---|---|---|---|
| pip | Slow | No (use pip-tools) | Basic | Manual | Legacy only |
| pipenv | Slow | Yes | Good | Automatic | No |
| Poetry | Moderate | Yes | Good | Automatic | Declining |
| uv | Very fast | Yes | Excellent | Automatic | Yes |
uv is a Rust-based Python package manager from the Astral team (who also make Ruff). It's 10-100x faster than pip, handles virtual environments automatically, manages Python versions, and has an excellent dependency resolver. It's compatible with pyproject.toml, requirements.txt, and pip's command-line interface.
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Create a new project
uv init my-project
cd my-project
# Add dependencies
uv add requests flask sqlalchemy
# Add dev dependencies
uv add --dev pytest ruff mypy
# Run a script (auto-creates venv, installs deps)
uv run python src/main.py
# Run a tool without installing it
uvx ruff check .
The killer feature of uv run is that it handles virtual environment creation and dependency installation automatically. You never manually activate a venv or run pip install again.
When Poetry Still Makes Sense
Poetry has a large installed base and some features uv doesn't fully replicate yet, particularly around publishing packages to PyPI (though uv's uv publish is improving). If your team already uses Poetry and it works, the migration cost may not be justified. But for new projects, start with uv.
Virtual Environments Explained
A virtual environment is an isolated Python installation. Each project gets its own set of installed packages, preventing conflicts between projects that need different versions of the same library.
# uv handles this automatically, but if you need to understand it:
# Create a venv
uv venv
# It creates .venv/ in your project directory
# Activate it (if not using uv run):
source .venv/bin/activate
# Or just use uv run, which activates automatically:
uv run pytest
Key points:
- Always use virtual environments. Never install packages into system Python.
- The
.venv/directory should be in your.gitignore. - uv and Poetry both manage virtual environments automatically. If you're manually running
python -m venv, you're doing extra work.
pyproject.toml Configuration
pyproject.toml is the standard configuration file for Python projects, replacing setup.py, setup.cfg, requirements.txt, and tool-specific config files. One file to rule them all.
[project]
name = "my-project"
version = "0.1.0"
description = "A useful project"
requires-python = ">=3.12"
dependencies = [
"flask>=3.0",
"sqlalchemy>=2.0",
"requests>=2.31",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.9",
"mypy>=1.13",
]
[project.scripts]
my-cli = "my_project.cli:main"
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
[tool.mypy]
python_version = "3.12"
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
This single file configures your project metadata, dependencies, CLI entry points, linting, type checking, and test runner. No more scattering configuration across half a dozen files.
Type Checking with mypy and pyright
Python's type hints are optional, but using them with a type checker catches real bugs. The two main options are mypy and pyright.
mypy
The original Python type checker. Mature, well-documented, and widely used.
uv add --dev mypy
uv run mypy src/
Start with strict mode in pyproject.toml:
[tool.mypy]
strict = true
warn_return_any = true
warn_unused_configs = true
Strict mode enables all optional checks: disallow_untyped_defs, disallow_any_generics, check_untyped_defs, etc. For new projects, this is the right starting point. For existing projects, enable checks incrementally using per-module overrides:
[[tool.mypy.overrides]]
module = "legacy_module.*"
disallow_untyped_defs = false
pyright
Microsoft's type checker, used by Pylance in VS Code. Faster than mypy and sometimes catches errors mypy misses (especially around type narrowing and generics).
uv add --dev pyright
uv run pyright src/
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"
Which to Choose
Use pyright if your team uses VS Code (you get identical checking in editor and CI). Use mypy if you need specific mypy plugins (Django, SQLAlchemy, Pydantic all have mypy plugins) or if your CI needs to match what other Python teams expect. Both are good choices. Pick one, not both.
Ruff for Linting and Formatting
Ruff is a Rust-based Python linter and formatter that replaces flake8, black, isort, pyflakes, pycodestyle, and dozens of flake8 plugins. It's 10-100x faster and configured from a single [tool.ruff] section in pyproject.toml.
# Lint
uv run ruff check .
# Lint with auto-fix
uv run ruff check --fix .
# Format (replaces black)
uv run ruff format .
Configuration
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort (import sorting)
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # Ruff-specific rules
]
[tool.ruff.lint.isort]
known-first-party = ["my_project"]
The select approach (opt-in) is better than enabling everything and using ignore (opt-out). Start with the rules above and add more as your codebase matures.
Replacing Your Existing Tools
| Old Tool | Ruff Equivalent |
|---|---|
| black | ruff format |
| isort | ruff check --select I --fix |
| flake8 | ruff check |
| flake8-bugbear | select = ["B"] |
| pyupgrade | select = ["UP"] |
| autoflake | select = ["F841"] |
The migration is usually painless. Ruff intentionally matches black's formatting style and flake8's rule codes.
Putting It All Together
A complete development setup for a new Python project:
# Create project
uv init my-project && cd my-project
# Add dependencies
uv add flask sqlalchemy
uv add --dev pytest ruff mypy
# Configure tools in pyproject.toml (see examples above)
# Development workflow
uv run ruff check --fix . # Lint
uv run ruff format . # Format
uv run mypy src/ # Type check
uv run pytest # Test
CI Configuration (GitHub Actions)
- uses: astral-sh/setup-uv@v5
- run: uv sync
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy src/
- run: uv run pytest
The astral-sh/setup-uv action installs uv and caches dependencies. The entire CI job typically runs in under 30 seconds for medium-sized projects.
Recommendations
- Use uv for all new Python projects. It's faster, simpler, and better designed than the alternatives.
- Configure everything in pyproject.toml. One file, one source of truth.
- Enable strict type checking from day one. Retrofitting types onto an untyped codebase is painful.
- Use Ruff for both linting and formatting. There's no reason to maintain separate flake8, black, and isort configurations anymore.
- Run all checks in CI:
ruff check,ruff format --check,mypy, andpytest. Fast tools mean no excuses for skipping checks.