# 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](https://pydevtools.com/handbook/reference/uv.md) (package management) |
| `package.json` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) |
| `package-lock.json` / `pnpm-lock.yaml` | `uv.lock` |
| `node_modules` | `.venv` (project-local isolated environment containing an interpreter and `site-packages`) |
| ESLint | [Ruff](https://pydevtools.com/handbook/reference/ruff.md) (linting) |
| Prettier | `ruff format` |
| `tsc --noEmit` (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) |
| Jest / Vitest | [pytest](https://pydevtools.com/handbook/reference/pytest.md) |
| `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](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) 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](https://pydevtools.com/handbook/explanation/pep.md)) from implementations, so multiple tools can implement the same standard in different ways. This is why [there are so many Python packaging tools](https://pydevtools.com/handbook/explanation/why-are-there-so-many-python-packaging-tools.md).

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](https://pydevtools.com/handbook/explanation/how-does-hot-reloading-work-in-python.md) for why, and [how to set up auto-reload](https://pydevtools.com/handbook/how-to/how-to-set-up-auto-reload-for-python-projects.md) 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](https://pydevtools.com/handbook/explanation/why-are-there-so-many-python-packaging-tools.md), and tools implement them.

## The Daily Development Loop

### Adding and Managing Dependencies

Adding a package works the way you'd expect:

```bash
# 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](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) (`uv.lock`). To install from an existing lockfile on a fresh clone:

```bash
uv sync                      # like npm install
```

### Running Python Code and Import Semantics

Running a script through uv:

```bash
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](https://pydevtools.com/handbook/explanation/what-is-an-editable-install.md) 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](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md) handles both linting and formatting in a single tool. It replaces the combination of ESLint and Prettier with one command:

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

```toml
[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](https://pydevtools.com/handbook/reference/mypy.md), [pyright](https://pydevtools.com/handbook/reference/pyright.md), and [ty](https://pydevtools.com/handbook/reference/ty.md). 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](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md) for help choosing.

```bash
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](https://pydevtools.com/handbook/reference/pytest.md) 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.

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

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

## Packaging and Distribution

### Library Packaging

Publishing a Python library to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) (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](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) like hatchling or setuptools.

```bash
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](https://pydevtools.com/handbook/reference/tox.md) and [nox](https://pydevtools.com/handbook/reference/nox.md) 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](https://github.com/taskipy/taskipy), or just document the commands in a README. The `--watch` flag that is everywhere in the Node world is [fragmented across frameworks](https://pydevtools.com/handbook/explanation/how-does-hot-reloading-work-in-python.md) in Python. And while uv workspaces exist, monorepo tooling comparable to Turborepo or Nx has not yet matured in the Python ecosystem.
