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 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 | Dependency resolution and installation |
Gemfile |
pyproject.toml dependencies under [project] |
Dependency declarations |
Gemfile.lock |
uv.lock |
Pinned dependency graph |
.gemspec |
pyproject.toml [project] |
Package metadata for publishing |
| RuboCop | Ruff | Linter and formatter |
| RSpec / Minitest | pytest | Test framework |
| Rake | No dominant equivalent | See Replacing Rake |
bundle exec |
uv run |
Run commands in the dependency context |
| RubyGems / rubygems.org | PyPI / pypi.org | Package registry |
gem build + gem push |
uv build + uv publish |
Build and publish a package |
| Sorbet / RBS | mypy / pyright / ty | 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 splits dependency declarations (Gemfile) from package metadata (.gemspec); Python’s pyproject.toml merges both, with the [project] table holding metadata and runtime dependencies and [dependency-groups] holding dev and test dependencies. Tool configuration for Ruff, pytest, and type checkers also lives here under [tool.*], 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. 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 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. 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: uv sync or uv run ensures the environment exists and matches the declared dependencies. The .venv directory plays the role of vendor/bundle but is always project-scoped. Its executables live in .venv/bin/ (.venv\Scripts\ on Windows) and run through uv run without manual activation.
pyproject.toml
pyproject.toml 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:
[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 lockfile (uv.lock). To install from an existing lockfile on a fresh clone:
uv sync # like bundle install (from lockfile)uv sync installs from the lockfile like bundle install, refreshing uv.lock first if it is stale. Pass --locked to fail instead when the lockfile is out of date (useful in CI), or --frozen to install from it as-is.
Python supports optional extras and dependency groups 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:
uv run python main.py # like bundle exec ruby main.rb
uv run main.py # also works for .py filesuv 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 distinguishes two execution modes that Ruby’s require hides: python file.py runs a single file, while python -m package runs a package as a module, which changes how imports resolve. 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:
[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 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 runwith explicit commands.uv run pytest --covreplacesrake test. No aliases, but tab completion and shell history reduce the friction.- Makefile. A
Makefilewith targets likemake testandmake lintis the closest analog to aRakefile. It works on macOS and Linux out of the box. Windows users needmakeinstalled or can use just as a cross-platform alternative. - nox or tox. 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 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:
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:
[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 who know Sorbet or RBS will find Python’s type-checking ecosystem more widely adopted. Python’s inline annotation syntax dates to Python 3.0, and PEP 484 standardized it for type hints in 3.5. Where Sorbet uses inline sig calls plus .rbi files and RBS uses separate .rbs files, Python annotations sit directly on function signatures and variables.
mypy is the original type checker and the most widely adopted. pyright is the type checker behind VS Code’s Pylance extension. ty is the newest, built by the team behind Ruff. See how they compare.
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 is Python’s dominant test framework. RSpec developers will notice a shift from BDD-style DSL (describe/it/expect) to plain functions and assertions:
# RSpec equivalent:
# describe "addition" do
# it "adds two numbers" do
# expect(1 + 1).to eq(2)
# end
# end
def test_addition():
assert 1 + 1 == 2No 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:
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.
uv run pytest # like bundle exec rspec
uv run pytest -v # verbose output
uv run pytest -x # stop on first failurePackaging and Distribution
Library Packaging
Publishing to PyPI (the Python Package Index) is analogous to gem push. 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 (like gem build myapp.gemspec)
uv publish # uploads to PyPI (like gem push myapp-0.1.0.gem)Where a gem bundles source and compiled extensions together, Python splits them: an sdist carries source, a wheel carries prebuilt binaries. When a compatible wheel exists, installation skips the build step, which is why pip install numpy finishes in seconds instead of compiling C. Without one, the installer builds from the 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
Dockerfilethat installs dependencies withuv sync --locked(or--frozenwhen the lockfile is already verified) and copies the source. - Platform-managed: services like Heroku, Railway, or Render that read
pyproject.tomland 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 runs a package’s CLI in an ephemeral, isolated environment without installing it into the project, similar to npx in the Node ecosystem:
uvx ruff check . # run ruff without installing itFor permanent installation, uv tool install creates an isolated environment per tool:
uv tool install ruff # like gem install rubocopWhat Python Offers, and What Ruby Developers Will Miss
Python’s REPL (and IPython) 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), but the notebook ecosystem is far more central to Python workflows. Tools like tox and nox 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 built-in task runner has no Python counterpart, so third-party alternatives mean choosing and configuring an extra tool. Rails’ convention-over-configuration project structure (migrations, asset pipeline, generators) has no standard-tooling equivalent; Django covers some of it, but Python favors explicit configuration over convention. And where bundle works identically across platforms, Python’s virtual environment activation differs by OS, though uv run bypasses activation entirely.