Skip to content

Python Tooling for Ruby Developers

Bundler manages dependencies, RuboCop enforces style, RSpec runs tests, and rbenv switches Ruby versions. Python organizes those jobs differently. uv manages dependencies, interpreters, and virtual environments, covering the roles of Bundler, rbenv, and ruby-build.

Map Ruby tools to Python

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.

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 lives under [tool.*], replacing files such as .rubocop.yml and .rspec.

In Ruby, Bundler defines the dependency format and implements the resolver. In Python, PEPs define interfaces for dependencies, builds, and metadata that multiple tools implement. This separation explains why so many packaging tools exist.

Separate the interpreter, environment, and project

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.

Manage the interpreter

uv python install 3.12 fills the role of rbenv install 3.2.4. A .python-version file pins the project’s interpreter, identical in concept to rbenv’s .ruby-version.

Isolate project dependencies

Ruby gems install into a shared or vendored location; bundle install --path vendor/bundle opts into project isolation. Python uses virtual environments, each with a link to an interpreter and its own site-packages folder. Separate environments prevent projects from sharing conflicting package versions.

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.

Configure the project in 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"]

Run the daily development workflow

Add and manage 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.

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

For the coverage command, add pytest-cov:

uv add --dev pytest-cov

The --dev flag marks pytest-cov as a development dependencyA package needed during development (testing, linting, formatting) that is not shipped to users of your project. Installed with the --dev flag. , keeping it out of production installs. The plugin adds the --cov option to pytest.

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

Lint and format code

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

Check types

Python annotations sit directly on function signatures and variables, following the type-hint standard PEP 484. Sorbet uses inline sig calls plus .rbi files; RBS uses separate .rbs files.

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.

Test 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 == 2

This test needs no imports or test class. 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 failure

Package and distribute projects

Build and publish libraries

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.

Deploy applications

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

Run 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 it

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

uv tool install ruff         # like gem install rubocop

Compare the remaining workflow differences

Python’s REPL and IPython support interactive development like IRB and Pry. Jupyter notebooks combine code, results, and prose for data science and machine learning; Ruby also has a kernel, IRuby.

Rails developers will find some familiar conventions in Django, but Python has no standard project structure covering migrations, asset pipelines, and generators. Teams choose those conventions alongside their tooling.

Last updated on