Reading Time: 11 minutes

Key Takeaways

  • uv has become the default Python package manager for scientific computing — 10 to 100 times faster than pip, with deterministic lockfiles that make research workflows reproducible.
  • Ruff replaces Black, isort, Flake8, pyupgrade, and autoflake in a single Rust-based binary. SciPy, Pandas, and other major scientific libraries use it today.
  • ty (released December 2025) is 20 to 100 times faster than mypy and uses a gradual guarantee that won’t break unannotated code — ideal for migrating research codebases.
  • pyproject.toml is now the single source of configuration for the entire toolchain, replacing scattered config files like .flake8, .isort.cfg, and mypy.ini.
  • For scientific computing specifically, conda remains necessary for non-Python dependencies (MPI, CUDA, HDF5), and Pyright is still the safer CI choice until ty reaches version 1.0.

If you are running scientific Python workflows in 2026, your toolchain has fundamentally changed. The packages you use for computation — NumPy, SciPy, FiPy, and the rest — are the same. But how you install, manage, lint, and type-check them is different from what most tutorials, older guides, and university lab notebooks still recommend.

Astral, the company behind Python’s most popular linter (Ruff), built three tools that now cover the entire developer workflow: uv for package management, Ruff for linting and formatting, and ty for type checking. Together, they replace pip, venv, Black, isort, Flake8, and mypy. That is six tools collapsed into three, all configured from a single pyproject.toml file.

This article covers the modern scientific Python toolchain in 2026. It explains what each tool does, why the shift happened, and how to set everything up — including the quirks and limitations that matter for scientific computing. If you are setting up a new simulation project, migrating an existing codebase, or catching up after the “Scientific Python Ecosystem” guide (see article #364), this is the practical update you need.

The Modern Tooling Landscape

Until about 2024, the scientific Python developer workflow looked like this:

  • Package management: pip with pip-tools or Poetry
  • Virtual environments: venv or virtualenv
  • Python version management: pyenv or pyenv-virtualenv
  • Linting: Flake8, then pydocstyle, plus pylint for style checks
  • Formatting: Black and isort (and later, autopep8 and pyupgrade)
  • Type checking: mypy

That meant installing six separate Python packages, maintaining separate configuration files, and waiting through sequential resolution steps. A simple pip install could take minutes. Running Black, then isort, then Flake8, then mypy could take even longer.

The shift started when Astral released Ruff in 2023. Ruff was written in Rust, designed to replace Black, isort, Flake8, pyupgrade, and autoflake simultaneously, and ran orders of magnitude faster than the Python-based alternatives. That adoption success gave Astral the momentum to build a full ecosystem: uv for package management and ty for type checking.

By late 2025 and early 2026, the convergence was complete. The Scientific Python Development Guide now officially recommends Ruff for style checks and ty for type checking. SciPy and Pandas have adopted Ruff. uv has overtaken Poetry in adoption among scientific computing teams. The old stack is not dead — it still works — but it is no longer the default for new projects.

Why pyproject.toml Matters

One of the biggest practical changes is the configuration model. The old stack scattered configuration across five or more files:

  • .flake8 for linting rules
  • .isort.cfg for import sorting
  • mypy.ini for type-checking behavior
  • setup.cfg for package metadata
  • pyproject.toml (partially, for build systems)

Modern tooling centralizes everything into pyproject.toml. A single file defines the package, the dependencies, the dev tools, and the tool configuration. This makes projects easier to share, clone, and maintain — exactly what research teams need when they publish code or onboard new students.

uv: Package Management That Actually Works

uv is a Rust-based package manager built by Astral. It replaces pip, pip-tools, pipx, pyenv, and virtualenv in a single fast binary. Unlike pip, uv does not require Python to be installed first — it can bootstrap a Python interpreter and manage versions alongside dependencies.

Why Researchers Are Switching

The primary reasons researchers and developers choose uv:

  1. Speed. uv installs packages 10 to 100 times faster than pip, mainly through parallel resolution and aggressive caching. This matters in CI pipelines where install time directly impacts developer turnaround.
  2. Lockfiles. A single uv.lock file records exact resolved versions of every dependency, including sub-dependencies. Committing this file to Git makes your environment fully reproducible — a requirement for published simulations.
  3. Python version management. uv can download and manage Python interpreters, eliminating the need for separate tools like pyenv.
  4. pip compatibility. Commands like uv pip install work with requirements.txt, making migration from existing projects simple.

Setting Up uv

The typical workflow for a new scientific Python project looks like this:

# Install uv (curl pipe to sh, cross-platform)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Create a project with a specific Python version
uv init my-simulation --python 3.11
cd my-simulation

# Add core scientific dependencies
uv add numpy scipy matplotlib

# Add domain-specific libraries
uv add fipy mpmath

# Add development tools
uv add --group dev pytest ruff ty

# Generate and commit a lockfile
uv lock
git add pyproject.toml uv.lock
git commit -m "Initial project with pinned dependencies"

uv sync installs from the lockfile, ensuring deterministic reproductions. Anyone cloning the repository and running uv sync gets the exact same resolved versions.

The Bytecode Compilation Quirk

Here is where uv behaves differently from pip, and where scientific computing teams have hit unexpected walls.

uv defers bytecode compilation to first-run. When uv installs a package, it stores precompiled bytecode in a cache but does not compile every file immediately. This makes uv installations approximately 3 to 4 times faster than pip. However, the first import of a library (especially NumPy or SciPy) can be roughly 2.5 times slower than a pip-installed copy, because the bytecode is compiled at import time.

For interactive development, this slowdown is usually imperceptible. For CI pipelines, HPC job scripts, or production servers that import NumPy or SciPy on every run, the quirk matters. The fix is simple: add --compile-bytecode to your sync command.

Real-world context: Plotly’s data team documented this exact issue after adopting uv in production. Their production servers saw noticeably slower NumPy imports until they added the flag. See Plotly’s blog post on uv quirks for the full technical breakdown.

The Exclusive-Index Behavior

uv treats --extra-index-url entries as exclusive by default, following PEP 0708. This means if a package exists in your primary index, uv will never check the extra index for it. This protects against dependency attacks — a malicious actor cannot replace a package hosted on a well-known index with a compromised mirror. But it also breaks existing requirements.txt pipelines that depend on extra indices for fallback packages.

If your lab uses a private package index or a conda-compatible index, you will need to configure the index-strategy in pyproject.toml explicitly. Without that configuration, uv may fail to resolve packages it expects to find on the extra index.

When uv Is Not Enough

uv manages Python packages and Python interpreters. It does not manage non-Python system dependencies: C and C++ libraries, Fortran compilers, MPI, CUDA toolkits, HDF5, FFTW, or graphics libraries.

For those dependencies, conda remains the standard. The recommended pattern is to use uv for Python packages and conda (or Pixi) for system-level dependencies. Many scientific teams pair the two tools — conda for the system stack, uv for the Python layer.

If your project involves non-Python dependencies such as MPI, CUDA, or HDF5, see the related guide on managing dependencies in scientific Python, which covers conda, lockfiles, and when to use each tool.

Ruff: The Linter That Replaces Six Tools

Ruff is a Rust-based linter and formatter built by Astral. It replaces Black (formatting), isort (import sorting), Flake8 (style checks), pyupgrade (dead-code removal), and autoflake (unused variable removal) in a single binary that runs 10 to 100 times faster than the combined old stack.

That makes it especially attractive for scientific computing, where large codebases with mixed Python versions and legacy modules can take several seconds to lint with the traditional tools. Ruff does it in milliseconds.

Scientific Libraries Already Use Ruff

Ruff is no longer just a web-framework linter. Major scientific libraries have adopted it:

  • SciPy — the numerical methods library core team migrated to Ruff.
  • Pandas — uses Ruff for style enforcement across the entire codebase.
  • FastAPI and Hugging Face — both use Ruff as their sole formatter and linter.

This adoption matters because it signals that Ruff handles the edge cases of scientific code — long docstrings, complex type annotations, legacy Python-2-style imports — without losing correctness or introducing formatting bugs. The official Ruff documentation lists all three libraries as adopters. See the official Ruff docs for the full list.

Configuration

Ruff configures entirely from pyproject.toml:

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "RUF"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"

The select array specifies which rule sets to enable. E and F cover PEP 8 errors and pyflakes checks. I handles import sorting (replacing isort). N enforces naming conventions. UP runs pyupgrade-style modernization. RUF adds Ruff-specific rules. The ignore line removes E501 (line-too-long) because Ruff delegates line-length to the formatter, keeping the linter fast.

Migration From Black + isort + Flake8

Removing the old stack is straightforward. After installing Ruff, you can replace the commands:

# OLD stack
black .
isort .
flake8 .
pyupgrade --py38 src/

# NEW stack
ruff check .
ruff format .

ruff check handles all style checks. ruff format handles all formatting. That is two commands instead of four, running one binary instead of four separate ones.

The Scientific Python Development Guide explicitly recommends migrating away from Flake8 to Ruff for style checks. See their security and development guide for the official recommendation.

ty: Type Checking Without the Pain

ty is Astral’s next-generation type checker, released in December 2025. It replaces mypy as the recommended static type checker in the Astral ecosystem. It is written in Rust and designed to be fast, strict-by-default, and compatible with unannotated code.

The Speed Win

The most dramatic advantage of ty is speed. In a real-world benchmark by a user who migrated from mypy to ty on an actual scientific Python codebase, mypy took 46 seconds and ty completed the same check in 2.19 seconds — roughly 20 times faster. See the full benchmark in the Stackademic migration post.

This matters because type checking is often the longest CI step in a Python workflow. Cutting 46 seconds down to 2.2 seconds reduces CI queue times, lets developers get faster feedback, and makes full type-checking feasible in branches where even slow type checkers would have been skipped.

The Gradual Guarantee

Unlike mypy, ty implements a gradual guarantee: it will not surface errors on code that has no annotations. If a module contains def calculate(x, y): return x + y with no type hints, ty treats it as untyped and does not complain about missing annotations. This is ideal for migrating scientific codebases that have partially typed modules — a common pattern in research code where the core simulation engine is typed but helper scripts and notebooks are not.

The official ty handbook explains the gradual guarantee in detail. The key point is that adding type annotations to a module does not cause ty to report errors in that module — it only reports errors in already-typed code. mypy does the opposite: it reports errors on any unannotated function it encounters, which breaks existing codebases that lack complete annotations.

Spec Conformance: The Tradeoff

Here is the tradeoff you need to understand before putting ty in CI.

According to a comprehensive comparison of mypy, pyright, ty, and pyre, ty’s Python typing spec conformance sits at approximately 53 percent, while Pyright achieves about 98 percent and mypy reaches about 58 percent. This means ty covers only half of the typing spec features, and it may miss edge cases that Pyright catches.

Until ty reaches version 1.0, the recommended CI strategy is a two-layer approach:

  1. Local development: use ty for fast feedback (2-second checks).
  2. CI pipelines: use Pyright for full spec coverage (catches ty misses).

This gives you both speed and correctness. Once ty reaches 1.0 and its spec conformance improves, you can rely on ty alone for CI.

Configuration

ty configures from pyproject.toml:

[tool.typer]
python-version = "3.11"
strict = true

The strict flag enables all strict mode checks (implicit column, no-untyped-def, etc.). See the ty handbook for the full configuration reference.

Migration Guide: From Old Stack to New

Here is the practical before-and-after comparison. If you are currently using pip, venv, Black, isort, Flake8, and mypy, this table shows exactly what replaces each tool.

Task Old Stack (2023 and earlier) Modern Stack (2025-2026) Notes
Package manager pip uv 10-100× faster installs, lockfiles included
Virtual environments venv / virtualenv Built into uv uv manages venvs automatically
Python version management pyenv / pyenv-virtualenv Built into uv uv downloads interpreters on demand
Style linting Flake8, pydocstyle Ruff Ruff replaces both in one binary
Formatting Black Ruff (ruff format) Same output as Black in most cases
Import sorting isort Ruff (ruff check –fix) Built into Ruff’s lint rules
Type checking mypy ty (local), Pyright (CI) ty is 20× faster; Pyright catches ty misses
Configuration 5+ scattered config files Single pyproject.toml All tools read from one file

Step-by-Step Migration

Here is the concrete migration path for an existing project:

# 1. Install uv and Ruff
uv pip install uv ruff

# 2. Create pyproject.toml (replacing setup.cfg)
cat >> pyproject.toml <<EOF
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.backends._deprecated"

[project]
name = "my-simulation"
version = "0.1.0"
dependencies = [
    "numpy",
    "scipy",
    "fipy",
]
EOF

# 3. Add Ruff config
cat >> pyproject.toml <<EOF

[tool.ruff]
line-length = 88
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
ignore = ["E501"]
EOF

# 4. Add ty config
cat >> pyproject.toml <<EOF

[tool.typer]
python-version = "3.11"
strict = true
EOF

# 5. Run uv sync to manage dependencies
uv sync

# 6. Run Ruff to check and format existing code
ruff check .
ruff format .

# 7. Run ty locally for fast type feedback
ty check .

This path works because Ruff’s output is nearly identical to Black’s formatting style, so the formatted code looks familiar. ty’s gradual guarantee means it will not break unannotated modules during the migration. You can add type annotations incrementally without fear of ty surfacing errors on code you have not yet typed.

If your project relies on requirements.txt, note that uv can install from it via uv pip install -r requirements.txt. But for long-term reproducibility, generate a uv.lock file and switch away from requirements.txt. See the related guide on managing dependencies in scientific Python for more on lockfiles and reproducibility.

What We Recommend: A Decision Framework

Not every team should adopt all three tools simultaneously. The right stack depends on your project’s requirements. Use this decision framework to choose:

  1. Do you need non-Python dependencies? (MPI, CUDA, HDF5, C++ libraries, Fortran compilers)
    • Yes: use conda for those dependencies. You can still use uv for the Python packages alongside conda.
    • No: continue to the next question.
  2. Are you publishing a Python package to PyPI?
    • Yes: consider Poetry for mature publishing workflows, or uv for faster installs during development. Both support PyPI publishing.
    • No: uv is the default choice for new research projects.
  3. Are you working in CI/CD pipelines where install time matters?
    • Yes: use uv. The speed gains (10-100× over pip) directly reduce CI queue times.
    • No: either uv or Poetry may work depending on team familiarity.
  4. How important is full type-checking spec coverage in CI?
    • High: use Pyright for CI, ty for local development. This gives you both speed and correctness.
    • Low: ty alone is sufficient for most research codebases.

For most new research projects, the recommended stack is:

  • uv for package management and lockfiles
  • Ruff for linting and formatting
  • ty for local type checking (fast feedback)
  • Pyright for CI type checking (full spec coverage)

That combination gives you speed, correctness, and reproducibility — the three pillars of research software quality.

Limitations: When to Stick With the Old Tools

The modern stack is powerful, but it is not a universal replacement. Here is when you should keep the old tools:

Conda for Non-Python Dependencies

uv manages Python packages and Python interpreters. It does not handle compiled C libraries, Fortran compilers, MPI, CUDA toolkits, HDF5, FFTW, or graphics libraries. For those, conda (or Pixi) remains the standard for scientific computing. Many teams use conda for system dependencies and uv for Python packages in the same environment.

mypy for Full Spec Coverage

Until ty reaches version 1.0 and closes its spec conformance gap, mypy or Pyright is the safer choice for CI environments that need complete type-checking coverage. Use ty for local development where speed matters and Pyright (or mypy) for CI where correctness matters.

Poetry for PyPI Publishing

If you publish scientific Python packages to PyPI, Poetry still has mature publishing workflows, dependency groups, and a well-documented build pipeline. uv supports PyPI publishing, but its workflows are newer and less documented than Poetry’s. If your team values established documentation and long production track records, Poetry may still be the better choice for the publishing side of your workflow.

Summary and Next Steps

The scientific Python toolchain has matured. The Rust-powered stack — uv, Ruff, and ty — replaces the old fragmented tools with something faster, simpler, and better integrated. Here are the takeaways:

  1. uv is the default package manager for new projects. Use --compile-bytecode on servers and CI pipelines. Use conda alongside it for non-Python dependencies.
  2. Ruff replaces Black, isort, Flake8, pyupgrade, and autoflake. SciPy and Pandas already use it. Configure everything from pyproject.toml.
  3. ty is the fastest type checker available — 20× faster than mypy. Use it locally. Use Pyright for CI until ty reaches 1.0.
  4. pyproject.toml is the single source of configuration. All three tools read from it. No more scattered config files.

If you are starting a new simulation project, adopt the modern stack from day one. If you are migrating an existing project, follow the step-by-step path above — Ruff’s formatting output is nearly identical to Black’s, and ty’s gradual guarantee means you will not break existing code during the transition.

For broader context on the scientific Python library stack that sits on top of this tooling, see the Scientific Python Ecosystem guide, which covers NumPy, SciPy, SymPy, Matplotlib, Jupyter, and scikit-learn. For deeper coverage of dependency management and lockfiles, see the Managing Dependencies in Scientific Python guide. And for maintenance best practices that pair well with the modern toolchain, read the Best Practices for Maintaining Scientific Code guide.

The scientific Python ecosystem is not going anywhere. But how you work with it has changed, and adopting the modern stack gives you faster builds, simpler configuration, and better reproducibility — all without changing the libraries you use for computation.

References and Further Reading