# Python Tooling for Ruby Developers


Bundler manages dependencies, RuboCop enforces style, RSpec runs tests, and rbenv switches Ruby versions. Python has equivalent tools for each of those jobs, though they are organized differently. [uv](https://pydevtools.com/handbook/reference/uv.md) consolidates dependency management, interpreter installation, and virtual environment handling into one command, covering territory that Ruby spreads across Bundler, rbenv, and ruby-build. This guide maps Ruby tooling concepts to their Python counterparts so Ruby developers can orient quickly.

## What Will Feel Familiar, and What Will Not

Most Ruby workflows have a recognizable Python equivalent:

| Ruby | Python | Notes |
|------|--------|-------|
| rbenv / ruby-build | `uv python install` + `uv python pin` | Interpreter version management |
| Bundler | [uv](https://pydevtools.com/handbook/reference/uv.md) | Dependency resolution and installation |
| `Gemfile` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) `dependencies` under `[project]` | Dependency declarations |
| `Gemfile.lock` | `uv.lock` | Pinned dependency graph |
| `.gemspec` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) `[project]` | Package metadata for publishing |
| RuboCop | [Ruff](https://pydevtools.com/handbook/reference/ruff.md) | Linter and formatter |
| RSpec / Minitest | [pytest](https://pydevtools.com/handbook/reference/pytest.md) | Test framework |
| Rake | No dominant equivalent | See [Replacing Rake](#replacing-rake) |
| `bundle exec` | `uv run` | Run commands in the dependency context |
| RubyGems / rubygems.org | [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) / pypi.org | Package registry |
| `gem build` + `gem push` | `uv build` + `uv publish` | Build and publish a package |
| Sorbet / RBS | [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 type checking (Sorbet checks types; RBS is a type-signature format) |

> [!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.

`pyproject.toml` replaces both `Gemfile` and `.gemspec`. Ruby separates dependency declarations (`Gemfile`, consumed by Bundler) from package metadata (`.gemspec`, consumed by RubyGems). Python's [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) merges both into one file: the `[project]` table holds metadata (name, version, authors) and a `dependencies` list for runtime dependencies, and `[dependency-groups]` handles development and test dependencies. Tool configuration for Ruff, pytest, and type checkers also lives in `pyproject.toml` under `[tool.*]` sections, consolidating what Ruby spreads across `.rubocop.yml`, `.rspec`, and separate config files.

Standards and tools are separate. In Ruby, Bundler defines the dependency format and implements the resolver. In Python, PEPs define interfaces (how to declare dependencies, how to build packages, how to specify metadata), and multiple tools implement those interfaces. This is why [so many packaging tools exist](https://pydevtools.com/handbook/explanation/why-are-there-so-many-python-packaging-tools.md). uv covers most of the surface area, but understanding that standards and tools are decoupled explains choices that otherwise seem arbitrary.

## Python's Three-Layer Model

Ruby keeps interpreter management (rbenv) separate from dependency management (Bundler), but Bundler reads the `Gemfile` and resolves gems against whatever Ruby version is active. Python separates three layers more explicitly.

### Interpreter Management

`uv python install 3.12` fills the role of `rbenv install 3.2.4`. It downloads and manages Python interpreters. A [`.python-version` file](https://pydevtools.com/handbook/explanation/what-is-a-python-version-file.md) pins the project's interpreter, identical in concept to rbenv's `.ruby-version`. The difference: uv handles both interpreter installation and dependency management, while Ruby requires rbenv (or rvm) for version management and Bundler for dependencies.

### The Virtual Environment

Ruby gems install into a shared or vendored location. `bundle install --path vendor/bundle` isolates gems per project, but this is opt-in. Python makes isolation the default through [virtual environments](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md). A virtual environment is a directory containing a link to a Python interpreter and its own `site-packages` folder where dependencies live. Without one, all projects share the same global set of packages, leading to version conflicts.

uv creates and manages virtual environments automatically. Running `uv sync` or `uv run` ensures the environment exists and matches the project's declared dependencies. The `.venv` directory serves a role similar to `vendor/bundle`, but it is always project-scoped. Unlike `vendor/bundle`, a virtual environment's executables live in `.venv/bin/` (or `.venv\Scripts\` on Windows) and can be invoked directly (`.venv/bin/python main.py`), through shell activation (`source .venv/bin/activate`), or through `uv run`, which handles this automatically.

### pyproject.toml

[pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) is the single file that replaces `Gemfile`, `.gemspec`, and most tool-specific config files. It declares the project name, version, dependencies, and Python version constraints. It also hosts configuration for other tools in `[tool.*]` tables:

```toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.31"]

[dependency-groups]
dev = ["pytest>=8.0", "ruff>=0.11"]

[tool.ruff]
line-length = 88

[tool.pytest.ini_options]
testpaths = ["tests"]
```

Compare this to a Ruby project that needs a `Gemfile`, a `.gemspec` (for libraries), `.rubocop.yml`, and `.rspec`. Python puts everything in one place.

## The Daily Development Loop

### Adding and Managing Dependencies

`uv add requests` works like adding `gem 'faraday'` to a `Gemfile` and running `bundle install` in one step. It 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 bundle install (from lock file)
```

`uv sync` is closer to `bundle install` than to `bundle update`: it resolves and installs dependencies from the lock file. If `uv.lock` is out of date, `uv sync` updates it first. To enforce that the lock file is current and fail if it is stale (useful in CI), pass `--locked`. To skip the freshness check and use the existing lock file as-is, pass `--frozen`.

Python supports [optional extras and dependency groups](https://pydevtools.com/handbook/explanation/what-are-optional-dependencies-and-dependency-groups.md) for separating development, testing, and documentation dependencies. Dependency groups fill the role of Bundler's groups (`group :development do ... end`), with the distinction that Python's groups are declared in `[dependency-groups]` tables. The `dev` group is synced by default; non-default groups are installed selectively with `uv sync --group docs` or `uv sync --only-group lint`.

### Running Python Code

Running a script through uv:

```bash
uv run python main.py        # like bundle exec ruby main.rb
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 same role as `bundle exec`, which ensures commands run against the project's bundled gems rather than system-wide installations.

Python has two execution modes that Ruby does not distinguish. `python file.py` runs a single file. `python -m package` runs a package as a module, which affects how imports resolve. Ruby's `require` and `require_relative` handle both cases transparently; Python's import system needs to know whether the code is a script or part of a package. When in doubt, prefer `uv run python -m mypackage`.

Console scripts (entry points defined in `pyproject.toml`) serve a similar role to binstubs or executable entries in a gemspec:

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

For library development, the [`src/` layout](https://pydevtools.com/handbook/explanation/src-layout-vs-flat-layout.md) 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 Rake

Rake is Ruby's built-in task runner, used for database migrations, code generation, and custom build steps. Python has no single dominant equivalent.

- **`uv run` with explicit commands.** `uv run pytest --cov` replaces `rake 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 a `Rakefile`. 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.
- **[nox](https://pydevtools.com/handbook/reference/nox.md) or [tox](https://pydevtools.com/handbook/reference/tox.md).** These test automation tools define sessions (nox) or environments (tox) in Python or config files. They excel at multi-version testing but are heavier than Rake for simple tasks.

Start with direct `uv run` commands; 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) combines linting and formatting into one tool, covering the territory RuboCop handles alone. Running `ruff check` lints, `ruff format` formats, and `ruff check --fix` auto-fixes:

```bash
uv run ruff check .          # lint (like rubocop)
uv run ruff format .         # format (like rubocop -A for layout cops)
```

RuboCop organizes its checks into departments (Layout, Lint, Style, Metrics, Naming, Security). Ruff uses a similar rule-code system where each rule belongs to a category (E for pycodestyle errors, F for pyflakes, I for import sorting, S for security). Configuration lives in `pyproject.toml`:

```toml
[tool.ruff.lint]
select = ["E", "F", "I", "S"]
```

Where RuboCop reads `.rubocop.yml` and allows per-directory overrides, Ruff reads `pyproject.toml` and supports per-file rule overrides through `[tool.ruff.lint.per-file-ignores]`.

### Type Checking

Ruby developers familiar with Sorbet or RBS will find Python's type-checking ecosystem more widely adopted. Python's function annotation syntax has been part of the language since Python 3.0, and PEP 484 (Python 3.5) standardized it for type hints. Annotations appear inline alongside function signatures and variable declarations. Ruby's Sorbet supports inline `sig` annotations in `.rb` files but also uses supplemental `.rbi` interface files; RBS uses separate `.rbs` signature files entirely.

[mypy](https://pydevtools.com/handbook/reference/mypy.md) is the original type checker and the most widely adopted. [pyright](https://pydevtools.com/handbook/reference/pyright.md) is the type checker behind VS Code's Pylance extension. [ty](https://pydevtools.com/handbook/reference/ty.md) is the newest, built by the team behind Ruff. See [how they compare](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md).

```bash
uv run mypy .
```

Type annotations are optional. Python itself runs code with zero annotations, but type checkers can still report errors in unannotated code through type inference. Because typing is gradual, most teams adopt it incrementally: add annotations to new code, tighten strictness over time.

### Testing with pytest

[pytest](https://pydevtools.com/handbook/reference/pytest.md) is Python's dominant test framework. RSpec developers will notice a shift from BDD-style DSL (`describe`/`it`/`expect`) to plain functions and assertions:

```python
# RSpec equivalent:
# describe "addition" do
#   it "adds two numbers" do
#     expect(1 + 1).to eq(2)
#   end
# end

def test_addition():
    assert 1 + 1 == 2
```

No imports needed, no test class required. pytest discovers tests by naming convention: files named `test_*.py` containing functions named `test_*`.

pytest fixtures replace RSpec's `let`, `before`, and `after` blocks with a dependency-injection model. A fixture is a function that provides test dependencies, and test functions declare which fixtures they need as parameters:

```python
def test_read_config(tmp_path):
    config_file = tmp_path / "config.toml"
    config_file.write_text('key = "value"')
    assert read_config(config_file)["key"] == "value"
```

`tmp_path` is a built-in fixture that creates a temporary directory. pytest wires it up automatically, the way RSpec's `let` blocks lazily evaluate and cache their values.

```bash
uv run pytest                # like bundle exec rspec
uv run pytest -v             # verbose output
uv run pytest -x             # stop on first failure
```

## Packaging and Distribution

### Library Packaging

Publishing to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) (the Python Package Index) is analogous to `gem push`. The package format differs: Python libraries are distributed as [wheels](https://pydevtools.com/handbook/reference/wheel.md) (prebuilt) and [sdists](https://pydevtools.com/handbook/reference/sdist.md) (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 (like gem build myapp.gemspec)
uv publish                   # uploads to PyPI (like gem push myapp-0.1.0.gem)
```

Ruby gems can be source-only or platform-specific (prebuilt for a target OS and architecture), but the gem format bundles source and compiled extensions together. Python separates these into two distinct formats: [sdists](https://pydevtools.com/handbook/reference/sdist.md) contain source code, and [wheels](https://pydevtools.com/handbook/reference/wheel.md) contain prebuilt binaries. When a compatible wheel exists, installation skips the build step entirely. This is why `pip install numpy` finishes in seconds rather than compiling C code from source. If no suitable wheel is available, the installer falls back to building from an sdist.

### Application Deployment

Ruby applications deploy as source code with dependencies installed via `bundle install` on the target, often managed by Capistrano, Docker, or a platform like Heroku. Python applications deploy the same way, with a virtual environment instead of bundled gems:

- Container-based: a `Dockerfile` that installs dependencies with `uv sync --locked` (or `--frozen` when the lock file is already verified) and copies the source.
- Platform-managed: services like Heroku, Railway, or Render that read `pyproject.toml` and install dependencies at deploy time.

Ruby developers using Puma or Unicorn for process management will find that gunicorn and uvicorn serve the same role for Python web applications: process supervision and worker management.

### CLI Tools

[`uvx`](https://pydevtools.com/handbook/reference/uvx.md) runs a package's CLI in an ephemeral, isolated environment without installing it into the project, similar to `npx` in the Node ecosystem:

```bash
uvx ruff check .             # run ruff without installing it
```

For permanent installation, `uv tool install` creates an isolated environment per tool:

```bash
uv tool install ruff         # like gem install rubocop
```

## What Python Offers, and What Ruby Developers Will Miss

Python's REPL (and [IPython](https://pydevtools.com/handbook/reference/ipython.md)) supports interactive development like IRB and Pry. Jupyter notebooks extend this into a development paradigm used across data science and machine learning. Ruby has a Jupyter kernel ([IRuby](https://github.com/SciRuby/iruby)), but the notebook ecosystem is far more central to Python workflows. 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, filling a gap that Ruby's ecosystem addresses with CI matrix builds but no dedicated local tool.

Ruby developers will miss Rake. The task runner that ships with every Ruby installation has no Python counterpart, and third-party alternatives require choosing and configuring an extra tool. Rails' convention-over-configuration approach to project structure (migrations, asset pipeline, generators) has no equivalent in Python's standard tooling; Django provides some of this, but the Python ecosystem as a whole favors explicit configuration over convention. And while `bundle exec` and `bundle install` work identically across macOS, Linux, and Windows, Python's virtual environment activation differs by platform (though `uv run` bypasses activation entirely, smoothing this out).
