Skip to content

Python Tooling for Node.js Developers

npm installs packages, locks versions, runs scripts, and publishes modules. Python splits those responsibilities differently, but uv 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.

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 Dependency management
package.json pyproject.toml 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 (linting)
Prettier ruff format
Jest / Mocha / Vitest pytest
npm run <script> No direct equivalent See 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 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 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 pins the project’s Python version, serving the same role as .nvmrc or .node-version.

Layer 2: The dependency sandbox. A virtual environment 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 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:

# 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 (uv.lock). To install from an existing lock file on a fresh clone:

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 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:

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:

[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 (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:

{
  "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 as a cross-platform alternative.
  • Third-party tools. 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 handles both linting and formatting. It replaces the combination of ESLint and Prettier with one command:

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:

[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 for a comparison with tsc.

mypy and pyright are the established type checkers. ty is a newer option from the team behind Ruff. See how they compare for help choosing.

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 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.
# 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.

uv run pytest                # like npx jest

Packaging and Distribution

Library Packaging

Publishing to PyPI (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.

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:

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:

uv tool install ruff         # like npm install -g eslint

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

Python’s REPL (and IPython) 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 and nox 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: web frameworks like Django and FastAPI provide their own reload mechanisms, but general-purpose file watching requires extra setup. And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.

Last updated on