# Python Tooling for Go Developers


Go ships one toolchain that handles compilation, dependency management, testing, formatting, and vetting. Python does not. That single difference explains most of the friction a Go developer will encounter when picking up Python, and most of that friction disappears once the mental model clicks.

## What Will Feel Familiar, and What Will Not

Many Go tools have Python counterparts, though the boundaries between them fall in different places.

| Go | Python equivalent | Notes |
|---|---|---|
| `go` (the toolchain) | [uv](https://pydevtools.com/handbook/reference/uv.md) | Closest single-tool equivalent |
| `go install golang.org/dl/go1.21@latest` | `uv python install 3.12` | Download and manage additional runtime versions |
| `go.mod` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) | Project metadata and dependency declarations |
| `go.sum` (integrity) / `go.mod` (versions) | `uv.lock` | `go.sum` stores hashes, not a full dependency graph; `uv.lock` pins the complete resolved tree |
| `go get` | `uv add` | Add a dependency |
| `go run` | `uv run` | Run code with dependencies available |
| `go test` | pytest (via `uv run pytest`) | Test runner |
| `go vet` / `staticcheck` | [Ruff](https://pydevtools.com/handbook/reference/ruff.md) | Linting |
| `gofmt` | `ruff format` | Code formatting |
| `goimports` | `ruff check --select I --fix` | Import sorting (ruff does not auto-add missing imports like `goimports`) |
| Go compiler type checking | [mypy](https://pydevtools.com/handbook/reference/mypy.md) / [pyright](https://pydevtools.com/handbook/reference/pyright.md) / [ty](https://pydevtools.com/handbook/reference/ty.md) | Optional, gradual |
| `go build` | No direct equivalent | Python runs from source; `uv build` creates distribution artifacts for publishing libraries |
| pkg.go.dev | [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) | pkg.go.dev is a docs/search portal; Go package publishing is decentralized via repositories. PyPI is a central registry |

> [!NOTE]
> These analogies are orientation aids, not exact equivalences. The boundaries between tools differ, and some concepts (like virtual environments) have no Go counterpart at all.

Two mental model gaps cause the most confusion for Go developers.

Tooling fragmentation and deployment. Go has one blessed toolchain. Python has shared standards ([PEPs](https://pydevtools.com/handbook/explanation/pep.md)) with multiple tool implementations, where the same task can be done by several tools conforming to the same specification. This is [why there are so many packaging tools](https://pydevtools.com/handbook/explanation/why-are-there-so-many-python-packaging-tools.md). The fragmentation extends to output: `go build` commonly produces a single deployable binary (though static linking is not guaranteed, especially with cgo), while Python programs run as source code inside an environment that provides the interpreter and dependencies.

Gradual typing. Go's compiler enforces types before any code runs. Python's type system is optional, and type checkers are separate tools that run alongside the code rather than gates that block execution.

## Python's Three-Layer Model

Go collapses runtime, dependency management, and project definition into `go`. Python separates these into three layers: the interpreter (runtime), the virtual environment (dependency sandbox), and the project file (`pyproject.toml`).

Interpreter management. Go developers who have used `go install golang.org/dl/go1.21@latest` to grab a second Go toolchain will find `uv python install` familiar. The workflow is the same: name the version you need and the tool fetches it.

```bash
uv python install 3.12
```

Multiple Python versions can coexist on one machine, and projects may require different versions. Where Go encodes the minimum toolchain version in the `go` directive of `go.mod`, Python projects pin the required version range in `pyproject.toml` under `requires-python` and can record the exact development version in a [`.python-version` file](https://pydevtools.com/handbook/explanation/what-is-a-python-version-file.md). When you run `uv run`, uv reads these files and downloads the right interpreter automatically, much like `go` fetches a newer toolchain when `go.mod` demands it.

Virtual environments. A [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) is an isolated directory of installed packages attached to a specific Python interpreter. It does not install Python; it depends on an existing interpreter. Where modern Go uses the module cache for downloaded dependencies, Python uses virtual environments to prevent one project's dependencies from colliding with another's. `uv` creates and manages these automatically when running `uv sync` or `uv run`.

pyproject.toml. This file serves the role of `go.mod`: it declares the project's name, version, Python version constraint, and dependencies. Unlike `go.mod`, it also holds configuration for tools like ruff, pytest, and mypy. One file replaces what Go spreads across `go.mod` and separate tool config files.

Python's tooling layer is built on PEP-defined interfaces, not a single toolchain. `uv` is one frontend to those interfaces, and others exist. All of them share core file formats like `pyproject.toml` and wheel/sdist standards, though some formats (like lock files) remain tool-specific. Switching between tools is possible for most workflows, though rarely necessary once you've picked one.

## The Daily Development Loop

### Adding and Managing Dependencies

Adding a dependency works like `go get`:

```bash
uv add requests
```

This updates `pyproject.toml` (like `go.mod`) and `uv.lock` (like `go.sum`). The [lock file](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) pins every transitive dependency to an exact version and hash.

One difference: Go resolves and installs dependencies implicitly when you build or run. Python makes the install step explicit. Running `uv sync` reads the lockfile and installs everything into the virtual environment. In practice, `uv run` calls `uv sync` automatically, so the experience feels similar to Go's implicit resolution.

### Running Python Code and Import Semantics

Running a script works as expected:

```bash
uv run python main.py
```

Or, if the file has a `.py` extension:

```bash
uv run main.py
```

This is analogous to `go run main.go`, with one addition: `uv run` ensures the virtual environment exists and dependencies are installed before executing.

Python has a distinction that Go lacks: `python file.py` and `python -m package` behave differently. Running a file directly executes that file as a script. Running with `-m` treats the argument as a package name and uses Python's import system to find it. This affects how relative imports resolve, and it trips up Go developers who expect a single, uniform way to run code.

Go uses a `main` package with a `main` function as the entry point. Python projects can define console scripts (entry points) in `pyproject.toml`, which generate executable commands when the package is installed. This is how tools like `ruff` and `pytest` become available as shell commands.

For developing a Python package, the `src/` layout with an [editable install](https://pydevtools.com/handbook/explanation/what-is-an-editable-install.md) is the closest analog to Go's seamless "edit code, run code" loop. An editable install links the source directory into the virtual environment so changes take effect without reinstallation.

### Code Quality: Linting and Formatting

[Ruff](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) handles both linting and formatting for Python, filling the roles of `go vet`, `staticcheck`, `gofmt`, and `goimports` in a single tool. Unlike Go, Python has no linter or formatter built into its toolchain; ruff is a third-party tool that has become the standard choice.

Add it as a development dependency and configure it in `pyproject.toml`:

```bash
uv add --dev ruff
```

```toml
[tool.ruff.lint]
select = ["E", "F", "I"]  # pyflakes, pycodestyle, import sorting
```

Run linting and formatting:

```bash
uv run ruff check .
uv run ruff format .
```

### Type Checking

Annotations can be added incrementally, and the interpreter ignores them entirely. This is the biggest conceptual shift for Go developers: Python code runs without any type annotations at all. Type checkers like [mypy, pyright, and ty](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) are separate tools that analyze annotations statically, producing warnings but never blocking execution.

A type checker treats annotated code more strictly and unannotated code more permissively. A large Python codebase might have thorough annotations in its core libraries and none in its scripts, and the type checker handles both.

```python
def greet(name: str) -> str:
    return f"Hello, {name}"

# This function has no annotations; the type checker
# will be lenient with it by default
def process(data):
    return data.strip()
```

There is no equivalent of Go's compile step that catches type errors before the program runs. Running `uv run mypy .` is the closest analog, but it's opt-in and typically wired into CI rather than blocking local development.

### Testing with pytest

[pytest](https://pydevtools.com/handbook/reference/pytest.md) is Python's standard test runner. The discovery conventions differ from `go test`:

| Go | Python (pytest) |
|---|---|
| `*_test.go` files | `test_*.py` and `*_test.py` files |
| `Test*` functions | `test_*` functions |
| `TestMain` for setup/teardown | Fixtures and hooks (pytest also supports xunit-style setup/teardown) |
| Built-in benchmarking (`b.N`) | `pytest-benchmark` plugin |
| Built-in race detector | No equivalent |

Run tests with:

```bash
uv run pytest
```

pytest's fixture system replaces Go's `TestMain` and setup/teardown patterns. Fixtures are functions that provide test dependencies (database connections, temporary files, HTTP clients) through dependency injection. A test declares what it needs by naming fixtures in its parameter list, and pytest wires them together automatically.

```python
import pytest

@pytest.fixture
def sample_user():
    return {"name": "Ada", "role": "engineer"}

def test_user_has_name(sample_user):
    assert sample_user["name"] == "Ada"
```

Go's built-in benchmarking and race detector have no direct equivalents in Python's standard test tooling. `pytest-benchmark` exists as a plugin, but profiling and concurrency testing in Python are separate concerns handled by different tools.

## Packaging and Distribution

### Library Packaging

Go libraries are typically packages distributed inside modules. Publishing means pushing a tagged commit to a repository. Python libraries need a build step.

A Python library is built into a distributable artifact (a wheel or sdist) using a [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) and published to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md), Python's central package registry. The build step compiles metadata and packages source code into a format that installers like `uv` and `pip` can consume.

```bash
uv build       # produces .whl and .tar.gz files
uv publish     # uploads to PyPI
```

This extra step exists because Python packages can contain compiled extensions (C, Fortran, Rust), platform-specific code, and metadata that needs to be resolved at build time. Go avoids this by compiling everything from source at install time.

### Application Deployment

Go commonly produces a single deployable binary. Copy it to a server and run it. Python does not work this way.

Python applications are deployed as source code plus an environment. The most common approaches:

- Containers: Build a Docker image that includes the Python interpreter, installed dependencies, and application source. This is the closest analog to Go's "single artifact" deployment.
- Virtual environment on the server: Sync dependencies with `uv sync` on the target machine, then run the application.

Tools like PyInstaller and Nuitka can produce standalone executables, but they are not part of the mainstream workflow. PyInstaller bundles the interpreter and dependencies together. Nuitka takes a different approach, compiling Python to C and producing native binaries.

This is a genuine architectural difference between the languages, not a tooling gap waiting to be filled. Python's runtime model requires an interpreter, and that interpreter needs to be present wherever the code runs.

## What Python Offers That Go Doesn't, and What You'll Miss

Python-unique strengths. The REPL (interactive interpreter) enables a style of exploratory development that has no Go equivalent. IPython extends this further. Jupyter notebooks combine code, output, and documentation 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 in a way that `go test` does not need to handle (since Go compiles to a binary, though toolchain version compatibility still matters in CI).

What you'll miss from Go. Single binary deployment is the most common answer. Go's "one way to do it" philosophy for tooling eliminates the decision fatigue that Python's ecosystem can produce. Compile-time type safety catches errors that Python's optional type system lets through. `go test`'s built-in benchmarking and race detector are conveniences that Python approximates only with third-party plugins and separate profiling tools.
