Skip to content

Python Tooling for TypeScript Developers

TypeScript developers moving to Python often expect a single tool like npm that handles everything. Python splits those responsibilities differently, and the reasons behind that split affect how every tool behaves. This guide maps TypeScript/JS concepts to their Python equivalents and covers the daily workflow.

What Will Feel Familiar, and What Will Not

Most TypeScript/JS tools have a reasonable Python counterpart. The table below provides a rough translation.

TypeScript / JavaScript Python
nvm / fnm uv Python version management (uv python install + uv python pin)
npm / pnpm / yarn uv (package management)
package.json pyproject.toml
package-lock.json / pnpm-lock.yaml uv.lock
node_modules .venv (project-local isolated environment containing an interpreter and site-packages)
ESLint ruff (linting)
Prettier ruff format
tsc --noEmit (type checking) mypy / pyright / ty
Jest / Vitest pytest
tsconfig.json [tool.*] sections in pyproject.toml (or dedicated config files like ruff.toml)
npx uvx
npm workspaces uv workspaces

Important

These analogies are orientation aids, not exact equivalences. Each Python tool has its own design philosophy and behavior that differs from the JS tool it loosely maps to.

Two mental model gaps trip up TypeScript developers more than anything else.

Virtual environments are not node_modules. A node_modules folder is just a directory of installed packages. A Python virtual environment is an isolated copy (or symlink) of a Python interpreter plus its own package directory. Deleting and recreating one is normal, and multiple environments can coexist on one machine for the same project.

Python separates standards from implementations. In the JS world, npm defines both the registry protocol and the dominant tool. Python separates standards (defined by PEPs) from implementations, so multiple tools can implement the same standard in different ways. This is why there are so many Python packaging tools.

One other difference worth noting: Python has no universal hot-reload convention. Node has nodemon, tsx --watch, and built-in --watch flags across many tools. Some Python frameworks (Django, Flask, FastAPI) provide their own reload mechanisms, but there is no cross-framework standard. See how hot reloading works in Python for why, and how to set up auto-reload for practical setup instructions.

Python’s Three-Layer Model

Node.js collapses several concerns into one tool: npm manages dependencies, runs scripts, and implicitly uses whatever Node version is on PATH. Python separates these into three distinct layers.

Layer 1: The runtime. Python version management is independent of package management. uv python install 3.12 installs a Python interpreter, similar to how nvm install 18 installs a Node version. But unlike nvm, the version manager and the package manager are the same tool (uv), even though the operations are conceptually separate.

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 Python interpreter; it does not install one. When you first run uv sync or uv run, uv creates and manages a .venv directory automatically, so for most workflows the virtual environment is invisible.

Layer 3: The project definition. pyproject.toml is the central project configuration file, similar to package.json. It declares dependencies, project metadata, and tool-specific settings. It has a [project.scripts] section for defining installed console entry points, but no built-in task-runner equivalent to package.json scripts. Tool configuration lives in [tool.*] sections (e.g., [tool.ruff], [tool.pytest.ini_options]) rather than in separate dotfiles.

This separation means you can change your Python version without touching project metadata, or swap out your package manager without changing your project file. You can also run the same project against multiple Python versions for testing (though dependency resolution may differ per version, requiring a re-sync). The flexibility comes from the standards-based approach: PEPs define interfaces, and tools implement them.

The Daily Development Loop

Adding and Managing Dependencies

Adding a package works the way you’d expect:

# Python (uv)               # TypeScript (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 lockfile on a fresh clone:

uv sync                      # like npm install

Running Python Code and Import Semantics

Running a script through uv:

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

Python has two ways to execute code that have no direct Node equivalent. python file.py runs a file directly. python -m package runs a package as a module, which matters for import resolution. In Node, node file.js handles both cases. In Python, the distinction affects whether relative imports work and how sys.path is constructed. When in doubt, prefer uv run python -m mypackage.

Python projects that expose command-line tools define console scripts (entry points) in pyproject.toml, analogous to the "bin" field in package.json. After installation, these are available as commands without needing python -m.

Many Python projects use a src/ layout where the package code lives in src/mypackage/ rather than at the project root. During development, the package is installed as an editable install so that code changes are reflected without reinstalling. uv sync handles this automatically when the project is configured as a package.

There is no universal --watch flag or nodemon equivalent. Web frameworks like Django and FastAPI include their own reload mechanisms, but for general Python scripts, automatic reload is not a standard feature.

Code Quality: Linting and Formatting

Ruff handles both linting and formatting in a single tool. It replaces the combination of ESLint and Prettier with one command:

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

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

[tool.ruff]
line-length = 88

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

Type Checking

TypeScript developers are used to types being inseparable from the language. Python’s type system is opt-in. Code runs whether or not it has type annotations, and type checking is a separate step performed by an external tool.

Three type checkers are in active use: mypy, pyright, and ty. There is no single tsc equivalent. Each checker interprets Python’s typing spec with slightly different strictness and speed tradeoffs. See how mypy, pyright, and ty compare for help choosing.

uv run mypy .                # like tsc --noEmit

Because typing is gradual, it is common to adopt it incrementally: add annotations to new code, tighten strictness over time, and leave legacy code untyped until it needs changes.

Testing with pytest

pytest is the dominant testing framework. Its design differs from Jest/Vitest 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_*. pytest also discovers Test* classes and supports unittest.TestCase tests.
  • Fixtures replace beforeEach/afterEach with a dependency-injection model.
# test_math.py
def test_addition():
    assert 1 + 1 == 2
uv run pytest                # like npx jest

Packaging and Distribution

Library Packaging

Publishing a Python library to PyPI (the Python Package Index) is analogous to npm publish. The package format is different: Python libraries are distributed as wheels (prebuilt) and sdists (source distributions), built by a build backend like hatchling or setuptools.

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

Application Deployment

Most Python applications are never “built” in the way a TypeScript app is bundled by webpack or esbuild. There is no equivalent of a JS bundle for typical Python server applications.

Instead, Python apps are deployed as source code plus a locked environment. The most common patterns are:

  • Container-based: A Dockerfile that installs dependencies with uv sync --frozen and copies the source.
  • Platform-managed: Services like Heroku or Railway that read pyproject.toml and install dependencies at deploy time.

The absence of a bundling step can feel strange coming from the JS world. Python’s import system loads modules at runtime from the filesystem, so there is nothing to resolve or tree-shake ahead of time for server applications.

What You Gain and What You Lose

Python’s REPL (and IPython) lets you test code interactively in ways that the node REPL rarely supports for real workflows. Jupyter notebooks extend this into an entire development and documentation paradigm, especially in data science. Tools like tox and nox also let you run test suites against multiple Python versions in a single command, something the JS ecosystem has no widely-adopted equivalent for.

On the other hand, TypeScript developers will miss the convenience of package.json scripts as a built-in task runner. Python has no equivalent; teams use Makefiles, taskipy, or just document the commands in a README. The --watch flag that is everywhere in the Node world is fragmented across frameworks in Python. And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.

Last updated on

Please submit corrections and feedback...