Python Tooling for Java Developers
Your first day writing Python, you will look for Maven and not find it. No single tool compiles code, resolves dependencies, runs tests, and packages artifacts in one standardized lifecycle. Python’s tooling is lighter and more modular, with small tools that each handle one concern. This article maps Java concepts to their Python counterparts and explains where the mental models diverge.
What Will Feel Familiar, and What Will Not
| Java | Python | Notes |
|---|---|---|
| SDKMAN (version installation) | uv python install |
Python version management (the JDK bundles runtime, compiler, and tools; uv python install only provides the interpreter) |
| Maven / Gradle | uv | Closest single tool for deps/envs/builds (but no standardized lifecycle phases) |
| pom.xml / build.gradle | pyproject.toml | Central project configuration file |
| Maven Central | PyPI | Public package registry |
| JUnit | pytest | Testing framework |
| Checkstyle / SpotBugs | ruff | Linting and some bug-pattern checks (partial SpotBugs overlap) |
| google-java-format | ruff format | Code formatting |
| Java’s static type checking | mypy / pyright / ty | Optional, gradual (separate tools, not a compiler) |
mvn package / mvn deploy |
uv build + uv publish |
uv build creates distribution artifacts for library publishing; Python has no WAR equivalent |
| JAR (library) | wheel / sdist | Distribution formats (scoped to library packaging) |
Important
These analogies are orientation aids, not exact equivalences. Each pairing hides real differences in scope and behavior.
Python has no standard build lifecycle. Maven and Gradle define phases (compile, test, package, deploy) that every project shares. Python has no equivalent. Tools are composed rather than prescribed, and most projects use only the pieces they need.
Python tooling is CLI-first, not IDE-first. IntelliJ drives much of the Java development experience: builds, refactoring, dependency management. Python tooling assumes the terminal. IDEs like VS Code and PyCharm integrate with CLI tools, not the other way around.
Most Python applications are also never “built.” There is no compile step, no artifact assembly. A Python web application typically runs from source, inside a virtual environment or a container. The concept of mvn package producing a deployable artifact has no direct parallel for most Python work.
Python’s Three-Layer Model
Java collapses several concerns into one: the JDK gives you a runtime, a compiler, and (via Maven/Gradle) a dependency resolution context. Python separates these into distinct layers.
Layer 1: The interpreter. A specific version of CPython (or PyPy, or another implementation), managed with uv python install 3.12. Unlike SDKMAN, which manages the entire JDK, Python version management only gives you an interpreter binary. It does not set up dependency isolation.
Layer 2: The virtual environment. A virtual environment is a lightweight directory that contains its own Python executable (via symlink or copy, depending on platform) and its own site-packages folder for dependencies. It is attached to an existing interpreter; it does not choose or install one.
Where Java uses classpath manipulation and module boundaries for isolation, Python uses separate filesystem directories. Every project gets its own environment, with its own installed packages.
Layer 3: The project definition. pyproject.toml declares metadata, dependencies, and tool configuration. It plays a role similar to pom.xml but is much lighter: no lifecycle phases, no plugin declarations, no inheritance hierarchies. It describes what the project needs; tools decide how to act on that description.
This separation explains why there are so many Python packaging tools. PEPs (Python Enhancement Proposals) define standard interfaces between these layers, and multiple tools can implement the same interface. Think of it like the Servlet specification (standard) with Tomcat and Jetty (implementations): the standard is shared, the tools compete on ergonomics and speed.
uv bridges all three layers. It installs Python versions, creates virtual environments, resolves dependencies, and runs scripts. For Java developers, it is the closest thing to a unified tool like Maven, though it makes fewer decisions about how to structure a project.
The Daily Development Loop
Adding and Managing Dependencies
In Maven, adding a dependency means editing pom.xml and running a build phase like mvn test or mvn package. In Python with uv:
uv add requestsThis updates pyproject.toml, resolves the dependency tree, writes a lock file (uv.lock), and installs the package into the virtual environment. To reproduce the exact environment from the lock file:
uv syncPython has no 1:1 mapping to Maven’s dependency scopes (compile, test, provided, runtime). Instead, it has runtime dependencies ([project.dependencies]), optional extras ([project.optional-dependencies]), build dependencies ([build-system.requires]), and dependency groups for dev/test use. Development-only packages typically go in a dependency group:
uv add --dev pytest ruffRunning Python Code and Import Semantics
There is no javac. Python code runs directly:
uv run python main.pyuv run ensures the virtual environment is active and dependencies are installed before executing the command.
Two ways to run Python code look similar but behave differently: python file.py executes a single file, while python -m package runs a package as a module, adjusting the import path. This distinction matters more than Java developers might expect, because Python’s import system is sensitive to how code is invoked.
Java applications declare a main class in the JAR manifest. Python’s equivalent is console scripts (also called entry points), declared in pyproject.toml:
[project.scripts]
myapp = "mypackage.cli:main"After installation, myapp becomes a command on the PATH.
For library development, the src/ layout keeps source code separate from project configuration and tests. Combined with an editable install, this lets code changes take effect without reinstalling. Python avoids Java’s classpath complexity, but import resolution has its own rules around packages, relative imports, and sys.path manipulation.
Code Quality: Linting and Formatting
Ruff handles both linting and formatting in a single tool. Where a Java project might use Checkstyle for style enforcement, SpotBugs for static analysis, and google-java-format for formatting, Python projects configure all of this in one section of pyproject.toml:
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
[tool.ruff.format]
quote-style = "double"No separate XML configuration files. No plugin dependencies to manage.
Type Checking
Java is statically typed, with mandatory type declarations on method signatures (though var enables local variable type inference since Java 10). The compiler enforces them. Python’s approach is the opposite: type annotations are optional, and the interpreter ignores them at runtime.
A Python program with zero type annotations runs without issue. Type checkers are separate tools that analyze code statically, outside the execution path. This is gradual typing: developers add annotations incrementally, and checkers also infer types in unannotated code to varying degrees, but annotations improve precision and coverage.
The main type checkers are mypy, pyright, and ty. Each interprets the typing specification slightly differently and makes different tradeoffs between strictness and convenience. None of them is “the compiler.”
Testing with pytest
Modern JUnit (5+) uses plain classes with @Test-annotated methods (older JUnit 3 required extending a base class). pytest takes a different approach: write plain functions whose names start with test_, and pytest discovers and runs them.
def test_addition():
assert 1 + 1 == 2No class hierarchy. No annotations. No assertion methods like assertEquals: Python’s built-in assert statement works, and pytest rewrites it at import time to produce detailed failure messages.
Where JUnit uses @BeforeEach and @AfterEach (or @Before/@After in JUnit 4) for setup and teardown, pytest uses fixtures: functions decorated with @pytest.fixture that provide test dependencies through injection. Fixtures provide dependency injection and scoping that many teams find more composable than lifecycle hooks, handling everything from temporary directories to database connections.
uv run pytestPackaging and Distribution
Library Packaging
When distributing a Python library for others to install, uv build produces two artifact types: a wheel (a pre-built package, analogous to a JAR) and an sdist (source distribution). The build backend declared in pyproject.toml controls how these artifacts are produced.
Publishing to PyPI is conceptually similar to publishing to Maven Central, though the tooling and verification processes differ. Where Maven has multi-module reactor builds for monorepos, Python has uv workspaces, which serve a similar purpose.
Application Deployment
Most Python applications skip the build step entirely. A web application written with Django or Flask does not produce a JAR or WAR equivalent. Instead, the application is deployed as source code alongside its environment: either a virtual environment on a server, or (more commonly now) a container image that installs dependencies at build time.
This is a genuine conceptual difference, not a missing feature. Python applications do not need compilation, and most deployment strategies rely on installing dependencies from a lock file into a clean environment rather than assembling a single distributable artifact.
What Python Offers That Java Does Not, and What You Will Miss
Python has strengths that the Java ecosystem lacks. REPL-driven development with IPython lets developers test ideas interactively, exploring APIs and debugging without writing test files. Jupyter notebooks extend this into a document format used widely for data analysis and exploratory work. For testing across multiple Python versions, tools like tox and nox automate matrix testing in a way that has no direct Maven equivalent.
Java developers will miss a few things. IntelliJ’s refactoring capabilities (rename across a project, extract method, inline variable) are stronger than what any Python IDE offers today, partly because static types give the refactoring engine more information to work with. The certainty of compile-time type checking is also gone: even with mypy or pyright configured strictly, the type checker is advisory rather than a gate the runtime enforces. The standardized build lifecycle of Maven or Gradle, where every project follows the same phases, has no Python counterpart, so each project assembles its own workflow from the tools available.