Python Tooling for Rust Developers
Cargo manages dependencies, builds code, runs tests, and publishes crates. Separate tools handle formatting (rustfmt, invoked via cargo fmt) and linting (clippy, via cargo clippy), while rustup manages toolchain installation. Python has no single tool with that scope, but uv comes closest. This article maps Rust tooling concepts to their Python counterparts so Rust developers can orient quickly.
What Will Feel Familiar, and What Will Not
Most Rust workflows have a recognizable Python equivalent:
| Rust | Python | Notes |
|---|---|---|
rustup |
uv python install |
Python version management |
| Cargo | uv | Closest single-tool equivalent |
Cargo.toml |
pyproject.toml |
Project metadata and dependencies |
Cargo.lock |
uv.lock |
Pinned dependency versions |
cargo add |
uv add |
Add a dependency |
cargo run |
uv run |
Run project code |
cargo test |
uv run pytest |
Run tests |
| clippy | ruff | Linter |
| rustfmt | ruff format |
Code formatter |
| Compiler type checking | mypy / pyright / ty | Optional, gradual |
cargo publish |
uv build + uv publish |
Build distribution artifacts and publish |
cargo build |
No direct equivalent | Python runs from source; no general app build step |
| crates.io | PyPI | Package registry |
Important
These analogies are orientation aids, not exact equivalences.
Two mental model gaps will affect daily work:
Gradual typing is a different contract. Python’s type system is optional. Code with zero annotations runs without complaint. A type checker is a separate tool, not the compiler, and it will not catch everything rustc would. Types are checked statically by an external tool or not at all; they have no runtime effect by default. And because there is no compilation step for pure Python, error detection shifts to runtime. Code executes immediately (CPython does compile to bytecode internally, and native extensions have real build steps, but there is no target/ directory, no debug vs. release profile, no link step). The feedback loop is faster, but fewer bugs are caught before the code runs.
Standards and tools are separate. In Rust, Cargo is the build system. In Python, PEPs define interfaces (how to declare dependencies, how to build packages, how to specify metadata), and multiple tools implement those interfaces. This is why so many packaging tools exist. uv covers most of the surface area, but understanding that standards and tools are decoupled explains choices that otherwise seem arbitrary.
Python’s Three-Layer Model
Cargo unifies three things that Python separates: the runtime (interpreter version), the dependency sandbox (virtual environment), and the project definition (pyproject.toml).
Interpreter Management
uv python install 3.12 fills the role of rustup toolchain install stable. It downloads and manages Python interpreters. Unlike rustup, Python’s interpreter manager and dependency manager were historically separate tools. uv merges them, but the conceptual separation still matters: a virtual environment is attached to an existing interpreter. It does not choose or install Python on its own.
The Virtual Environment
Rust’s Cargo stores compiled artifacts in a per-project target/ directory. Python uses virtual environments to isolate installed packages per project. Each virtual environment contains a link to a Python interpreter and its own site-packages directory where dependencies live. Without one, all projects share the same global set of packages, leading to version conflicts.
uv creates and manages virtual environments automatically. Running uv sync or uv run ensures the environment exists and matches the project’s declared dependencies.
pyproject.toml
pyproject.toml is Python’s Cargo.toml. It declares the project name, version, dependencies, and Python version constraints. It also hosts configuration for other tools (ruff, mypy, pytest) in [tool.*] tables, as if clippy and rustfmt settings lived inside Cargo.toml.
Unlike Cargo.toml, the format is governed by multiple PEPs rather than a single tool’s specification. PEP 621 defines the [project] table. Build system configuration, dependency groups, and tool settings each have their own standards. This is part of why there are so many packaging tools: the standards define what goes in the file, and different tools implement the machinery behind it.
The Daily Development Loop
Adding and Managing Dependencies
uv add requests works like cargo add reqwest. It updates pyproject.toml and the lock file in one step. uv sync installs everything from the lock file, comparable to building from Cargo.lock.
Python has no equivalent of Cargo’s feature flags. Instead, libraries declare optional extras (under [project.optional-dependencies]) that consumers can request at install time, such as uv add "httpx[http2]". Separately, projects can define dependency groups (under [dependency-groups]) for development, testing, or documentation dependencies. Extras are published package metadata; dependency groups are not.
Running Python Code and Import Semantics
uv run python main.py is the closest analog to cargo run. There is no build step; the interpreter reads and executes source files directly.
Python has two ways to run code that Rust has no equivalent for: python file.py runs a single script, while python -m package runs a package as a module, which affects how imports resolve. The distinction matters when a project uses relative imports.
Console scripts (entry points defined in pyproject.toml) serve a similar role to Cargo’s [[bin]] targets. They create named commands that invoke specific functions, installed into the environment’s bin/ directory.
Packages commonly put source code in a src/ directory, though flat layouts are also valid and widely used. During development, an editable install (uv sync handles this) lets changes to source files take effect immediately without reinstalling. Cargo handles this implicitly; Python requires the editable install mechanism because the interpreter needs packages to be “installed” to find them through its import system.
Code Quality: Linting and Formatting
Ruff combines linting and formatting into one tool. It covers the territory of both clippy and rustfmt. Running ruff check lints, ruff format formats, and ruff check --fix auto-fixes. Configuration lives in pyproject.toml under [tool.ruff].
Ruff re-implements rules from dozens of older Python linting tools, so most projects need only this single dependency for code quality enforcement.
Type Checking
This is the biggest adjustment for Rust developers. Python’s type system is optional and gradual. Code with zero type annotations executes without issue. The type checker is a standalone tool that reads annotations and reports inconsistencies, but it is not required to run the code.
Even with full annotations, Python’s type system cannot express ownership, lifetimes, or borrow semantics. It catches a different class of bugs: wrong argument types, missing return values, attribute access on None, and similar logical errors. It will not prevent data races or guarantee memory safety.
Three type checkers are in active use: mypy, pyright, and ty. mypy is the original and most widely adopted. pyright is the type-checking engine behind Pylance, VS Code’s default Python language server. ty is the newest, built by the same team behind ruff and uv, and offers the fastest performance.
Testing with pytest
pytest is Python’s standard test runner. Like cargo test, it discovers tests by convention rather than requiring explicit registration. The conventions differ: pytest looks for files named test_*.py (and *_test.py) and functions named test_*, rather than functions annotated with #[test].
def test_addition():
assert 1 + 1 == 2That is a complete test. No imports, no decorators, no test class required. pytest rewrites assert statements to provide detailed failure messages.
pytest fixtures replace Rust’s setup/teardown patterns. A fixture is a function that provides test dependencies (database connections, temporary files, mock services) through dependency injection.
Tests declare which fixtures they need as function parameters, and pytest wires them up automatically.
def test_read_config(tmp_path):
config_file = tmp_path / "config.toml"
config_file.write_text('key = "value"')
assert read_config(config_file)["key"] == "value"Run tests with uv run pytest. Add -v for verbose output, -x to stop on the first failure.
Packaging and Distribution
Library Packaging
Rust crates are source distributions that get compiled on the consumer’s machine. Python packages come in two forms: source distributions (sdists) and wheels. When a compatible wheel exists, installation skips the source build step, which is why pip install numpy finishes in seconds rather than compiling C and Fortran code. If no suitable wheel is available, the installer falls back to building from an sdist. A build backend handles creating both formats.
Publishing to PyPI is the equivalent of cargo publish to crates.io. uv build creates the distribution files, and uv publish uploads them.
Application Deployment
Rust produces a compiled native binary (static vs. dynamic linkage depends on target and flags). Ship it and the job is done.
Python applications are deployed as source code plus an environment: a virtual environment with installed dependencies, typically inside a container image, or on a server with a process manager. There is no single-binary output by default.
For CLI tools, pipx install and uv tool install provide something like cargo install, creating an isolated environment per tool so that end users can run Python CLI applications without managing dependencies themselves. (uvx is uv tool run, which runs tools in temporary environments without permanent installation.) This is limited to the Python ecosystem; it does not produce standalone binaries.
Tools like PyInstaller (bundler) and Nuitka (Python-to-C compiler) can produce standalone executables, but the results have different portability and runtime tradeoffs compared to Rust-native binaries.
Native Extensions
Rust developers writing Python extensions will find PyO3 and maturin. PyO3 provides Rust bindings for the Python C API. maturin is a build tool that compiles Rust code into Python extension modules, integrated with Python’s build system standards so that uv build works with Rust extensions out of the box. This is relevant for performance-critical libraries but is not the typical Python development workflow.
What Python Offers, and What Rust Developers Will Miss
Python brings capabilities that Rust’s ecosystem lacks. REPL-driven development through IPython allows testing ideas interactively without writing a file, and Jupyter notebooks combine code, prose, and visualizations in a single document, which is why Python dominates data science and machine learning. Tools like tox and nox automate testing across multiple Python versions, useful for library authors who need to support a range of interpreters.
Rust developers will miss the compiler as a safety net. Ownership, lifetimes, exhaustive pattern matching, and guaranteed memory safety have no Python equivalent. Cargo’s unified, opinionated workflow is another loss; even with uv covering most of the surface area, Python’s ecosystem reflects decades of independent tools built against shared standards rather than one cohesive design. Compile-time guarantees in general are traded for the speed and flexibility of an interpreted language.