← All articles
LANGUAGES Python Development Environment: Package Managers, Ty... 2026-02-09 · 5 min read · python · uv · ruff

Python Development Environment: Package Managers, Type Checking, and Linting

Languages 2026-02-09 · 5 min read python uv ruff mypy virtual-environments

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:

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