# Python Tooling for Node.js Developers


npm installs packages, locks versions, runs scripts, and publishes modules. Python splits those responsibilities differently, but [uv](https://pydevtools.com/handbook/reference/uv.md) consolidates most of them into a single command. This guide maps Node.js concepts to their Python equivalents, with emphasis on `npm` scripts and the daily runtime workflow. For `tsc`-style type-checking comparisons, see the companion [TypeScript developers guide](https://pydevtools.com/handbook/explanation/python-tooling-for-typescript-developers.md).

## What Will Feel Familiar, and What Will Not

Most Node.js workflows have a recognizable Python counterpart:

| Node.js | Python | Notes |
|---------|--------|-------|
| nvm / fnm / Volta | `uv python install` + `uv python pin` | Python version management |
| npm / pnpm / yarn | [uv](https://pydevtools.com/handbook/reference/uv.md) | Dependency management |
| `package.json` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) | Project metadata and dependencies |
| `package-lock.json` / `pnpm-lock.yaml` | `uv.lock` | Pinned dependency graph |
| `node_modules` | `.venv` (virtual environment) | Per-project isolation |
| `npx` | `uvx` | Run a CLI tool without permanent installation |
| ESLint | [Ruff](https://pydevtools.com/handbook/reference/ruff.md) (linting) | |
| Prettier | `ruff format` | |
| Jest / Mocha / Vitest | [pytest](https://pydevtools.com/handbook/reference/pytest.md) | |
| `npm run <script>` | No direct equivalent | See [Replacing npm scripts](#replacing-npm-scripts) |
| `npm publish` | `uv build` + `uv publish` | Build step is separate |
| nodemon | Framework-specific reloaders | No universal `--watch` |

> [!IMPORTANT]
> These analogies are orientation aids, not exact equivalences. Each pairing hides real differences in scope and behavior.

Two mental model gaps affect daily work.

Virtual environments are not `node_modules`. A `node_modules` folder holds downloaded packages for one project. A Python [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) goes further: it contains a Python interpreter (or symlink to one) and its own package directory, forming a self-contained execution context. Deleting and recreating one is normal. Multiple environments can coexist on one machine for the same project, each pinned to a different Python version.

There is no built-in task runner. `package.json` scripts serve as the task runner for Node.js projects: `npm run dev`, `npm test`, `npm run build`. Python's [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) has no equivalent section. Teams use `uv run` with explicit commands or Makefiles. This is the gap Node.js developers notice first.

## Python's Three-Layer Model

Node.js collapses several concerns into one tool: npm manages dependencies and runs scripts while using whatever Node version is on `PATH`. Python separates these into three layers.

Layer 1: The interpreter. `uv python install 3.12` installs a Python runtime, similar to `nvm install 20` or `fnm install 20`. A [`.python-version` file](https://pydevtools.com/handbook/explanation/what-is-a-python-version-file.md) pins the project's Python version, serving the same role as `.nvmrc` or `.node-version`.

Layer 2: The dependency sandbox. A [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) is an isolated directory containing a Python interpreter (or symlink to one) and its own set of installed packages. It is *attached to* an existing interpreter; it does not install one. When you first run `uv sync` or `uv run`, uv creates and manages a `.venv` directory automatically, so the virtual environment is invisible for most workflows.

Layer 3: The project definition. [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) declares dependencies and project metadata. Tool configuration lives in `[tool.*]` sections (e.g., `[tool.ruff]`, `[tool.pytest.ini_options]`) rather than in separate dotfiles, consolidating what Node.js spreads across `.eslintrc` and `.prettierrc`.

This three-layer separation explains why uv feels like nvm, npm, and npx merged into one tool: it bridges all three layers, installing Python versions, creating virtual environments, resolving dependencies, and running commands.

## The Daily Development Loop

### Adding and Managing Dependencies

Adding a dependency mirrors the npm workflow:

```bash
# Python (uv)               # Node.js (npm)
uv add requests              # npm install axios
uv add --dev pytest          # npm install --save-dev jest
```

`uv add` updates `pyproject.toml` and writes a [lock file](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) (`uv.lock`). To install from an existing lock file on a fresh clone:

```bash
uv sync                      # like npm ci
```

`uv sync` is closer to `npm ci` than `npm install`: it installs exactly what the lock file specifies without modifying it. Running `uv add` is the equivalent of `npm install <package>`, which both installs and updates `package.json`.

Python supports [optional extras and dependency groups](https://pydevtools.com/handbook/explanation/what-are-optional-dependencies-and-dependency-groups.md) for separating dev and test dependencies. These serve the same purpose as splitting `dependencies` from `devDependencies` in `package.json`, with more flexibility for additional groups.

### Running Python Code

Running a script through uv:

```bash
uv run python main.py
uv run main.py               # also works for .py files
```

`uv run` ensures the virtual environment exists and dependencies are installed before executing. It fills the role that `node` execution and `node_modules` resolution handle together.

Python has two execution modes that affect import resolution. `python file.py` runs a single file. `python -m package` runs a package as a module, which adjusts how imports resolve. Node.js developers familiar with the CommonJS `require()` and ESM `import` split will recognize the shape of this problem: how you invoke code affects what it can find. When in doubt, prefer `uv run python -m mypackage`.

Console scripts (entry points defined in `pyproject.toml`) serve a similar role to the `"bin"` field in `package.json`. They create named commands installed into the environment's `bin/` directory:

```toml
[project.scripts]
myapp = "mypackage.cli:main"
```

For library development, the `src/` layout keeps source code separate from configuration and tests. Combined with an [editable install](https://pydevtools.com/handbook/explanation/what-is-an-editable-install.md) (which `uv sync` handles automatically), code changes take effect without reinstalling.

### Replacing npm Scripts

This is the gap Node.js developers feel most. A typical `package.json` might have:

```json
{
  "scripts": {
    "dev": "nodemon server.js",
    "test": "jest --coverage",
    "lint": "eslint . && prettier --check .",
    "start": "node server.js"
  }
}
```

Python's `pyproject.toml` has no equivalent section. The practical default is to run commands directly with `uv run` and add a Makefile only when you want short aliases:

- **`uv run` with explicit commands.** `uv run pytest --cov` replaces `npm test`. No aliases, but tab completion and shell history reduce the friction.
- **Makefile.** A `Makefile` with targets like `make test` and `make lint` is the closest analog to `npm run`. It works on macOS and Linux out of the box. Windows users need `make` installed or can use [just](https://github.com/casey/just) as a cross-platform alternative.
- **Third-party tools.** [taskipy](https://github.com/taskipy/taskipy) adds a `[tool.taskipy.tasks]` section to `pyproject.toml`, which is the closest structural match to `package.json` scripts.

Start with direct `uv run` commands like `uv run pytest` and `uv run ruff check .`; add a Makefile only when the command list feels repetitive.

### Code Quality: Linting and Formatting

[Ruff](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) handles both linting and formatting. It replaces the combination of ESLint and Prettier with one command:

```bash
uv run ruff check .          # lint (like npx eslint .)
uv run ruff format .         # format (like npx prettier --write .)
```

Configuration lives in `pyproject.toml` under `[tool.ruff]`, not in separate config files:

```toml
[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]    # enable specific rule sets
```

Where Node.js projects juggle `.eslintrc`, `.prettierrc`, and sometimes conflicts between the two, Ruff handles both concerns with zero configuration overlap.

### Type Checking

Node.js developers who write plain JavaScript can skip this section and return when they want to add type annotations. Python's type system is opt-in: code runs whether or not it has annotations.

For TypeScript developers, see the [TypeScript guide's type-checking section](https://pydevtools.com/handbook/explanation/python-tooling-for-typescript-developers.md) for a comparison with `tsc`.

[mypy](https://pydevtools.com/handbook/reference/mypy.md) and [pyright](https://pydevtools.com/handbook/reference/pyright.md) are the established type checkers. [ty](https://pydevtools.com/handbook/reference/ty.md) is a newer option from the team behind Ruff. See [how they compare](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) for help choosing.

```bash
uv run mypy .
```

Because typing is gradual, most teams adopt it incrementally: add annotations to new code and tighten strictness over time. Legacy code often stays untyped until it needs changes.

### Testing with pytest

[pytest](https://pydevtools.com/handbook/reference/pytest.md) is Python's standard testing framework. Its design differs from Jest and Mocha in a few ways:

- Tests are plain functions prefixed with `test_`, not wrapped in `describe`/`it` blocks.
- Test discovery is by naming convention: files named `test_*.py` (and `*_test.py`) containing functions named `test_*`.
- Fixtures replace `beforeEach`/`afterEach` with a dependency-injection model.

```python
# test_math.py
def test_addition():
    assert 1 + 1 == 2
```

No `expect()` or assertion library needed. Python's built-in `assert` statement works directly, and pytest rewrites it at import time to produce detailed failure messages.

```bash
uv run pytest                # like npx jest
```

## Packaging and Distribution

### Library Packaging

Publishing to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) (the Python Package Index) is analogous to `npm publish`. The package format differs: Python libraries are distributed as wheels (prebuilt) and sdists (source distributions), built by a [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md).

```bash
uv build                     # creates wheel and sdist
uv publish                   # uploads to PyPI
```

npm publishes whatever is in your project directory (minus `.npmignore` entries). Python's separate build step exists because packages can contain compiled extensions (C, Fortran, Rust) that need platform-specific handling. For pure-Python packages, the build step adds a few seconds at most.

### Application Deployment

Node.js applications deploy as source code with `node_modules` installed on the target. Python applications deploy the same way, with a virtual environment instead of `node_modules`.

The most common patterns:

- Container-based: a `Dockerfile` that installs dependencies with `uv sync --frozen` and copies the source.
- Platform-managed: services like Heroku, Railway, or Render that read `pyproject.toml` and install dependencies at deploy time.

Node.js developers using pm2 or forever for process management will find that gunicorn and uvicorn serve the same role for Python web applications: process supervision and worker management.

There is no bundling step. Python's import system loads modules at runtime from the filesystem, so the concept of webpack or esbuild does not apply to server-side Python.

### CLI Tools

`npx` runs a package's binary without installing it globally. `uvx` does the same:

```bash
uvx ruff check .             # like npx eslint .
```

For permanent installation, `uv tool install` creates an isolated environment per tool, avoiding the dependency conflicts that `npm install -g` can cause:

```bash
uv tool install ruff         # like npm install -g eslint
```

## What Python Offers, and What Node.js Developers Will Miss

Python's REPL (and [IPython](https://pydevtools.com/handbook/reference/ipython.md)) supports interactive development in ways that Node's REPL cannot match for real workflows. Jupyter notebooks extend this into a development paradigm used across data science and machine learning. Tools like [tox](https://pydevtools.com/handbook/reference/tox.md) and [nox](https://pydevtools.com/handbook/reference/nox.md) automate testing across multiple Python versions, something the Node.js ecosystem has no equivalent for.

Node.js developers will miss `package.json` scripts most. The built-in task runner that comes free with every npm project has no Python counterpart, and third-party alternatives require choosing and configuring an extra tool. The `--watch` flag that ships with Node 18+ and tools like nodemon has [no universal Python equivalent](https://pydevtools.com/handbook/explanation/how-does-hot-reloading-work-in-python.md): web frameworks like Django and FastAPI provide their own reload mechanisms, but general-purpose file watching requires [extra setup](https://pydevtools.com/handbook/how-to/how-to-set-up-auto-reload-for-python-projects.md). And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.
