From Maven to pip: Python Tooling, Testing, and Packaging for Java Developers
Part 3 of the Python for Java Developers track. Last updated: October 2026.
You can write Python now — but can you ship it? Java gave you Maven, JUnit, and jar files as one coherent story. Python's story is equally coherent once you see the mapping: virtual environments instead of local repositories, pip instead of Maven, pytest instead of JUnit, and pyproject.toml instead of pom.xml. This post completes the track — and the whole Python roadmap.
Virtual environments: your local repository
Java developers never install dependencies globally — Maven scopes them per project. Python's equivalent is the virtual environment: an isolated directory holding one project's interpreter and packages. Create one per project, activate it, and everything you pip install stays inside:
# Create and activate a virtual environment (run in your shell): # python -m venv .venv # source .venv/bin/activate # Windows: .venv\Scripts\activate # Your prompt now shows (.venv) — pip installs go here, not system-wide. import sys print(sys.prefix) # points at .venv when activated # /home/you/northwind-demo/.venv
pip and requirements: Maven dependencies, simplified
pip is Maven's dependency resolver without the XML. Install from PyPI (the Central Repository of Python), then freeze exact versions into requirements.txt so every machine — and every Docker build — installs the identical set:
# Shell, inside the activated venv:
# pip install requests pandas streamlit
# pip freeze > requirements.txt
# requirements.txt — the Python pom.xml (dependencies only):
# requests==2.32.3
# pandas==2.2.3
# streamlit==1.32.0
# Reproduce the exact environment anywhere:
# pip install -r requirements.txt
import importlib.metadata
print(importlib.metadata.version("pip"))
# 24.2
pytest: JUnit without the boilerplate
pytest is JUnit reimagined: test functions instead of test classes, plain assert instead of assertion methods, and fixtures instead of @BeforeEach setup. Run with pytest, and failures show the values that broke — no expected:<…> but was:<…> decoding:
// Java + JUnit 5
@Test
void normalizesSku() {
assertEquals("ABC-1", SkuUtil.normalize(" abc-1 "));
}
# Python + pytest — test_orders.py
from orders import normalize_sku
def test_normalizes_sku(): # plain function, plain assert
assert normalize_sku(" abc-1 ") == "ABC-1"
def test_rejects_empty():
assert normalize_sku("") is None
# Run: pytest -q
# 2 passed in 0.03s
Project layout: the Maven standard directory, Python edition
Maven's src/main/java convention has a Python twin. A professional small project looks like this — generate the skeleton from Python so every new project starts the same way:
from pathlib import Path
skeleton = [
"pyproject.toml", # metadata + build config (the pom.xml)
"requirements.txt", # pinned runtime dependencies
"README.md",
"src/northwind/__init__.py", # the importable package
"src/northwind/pipeline.py",
"tests/test_pipeline.py", # pytest discovers test_*.py
]
for f in skeleton:
p = Path("northwind-demo") / f
p.parent.mkdir(parents=True, exist_ok=True)
p.touch(exist_ok=True)
print("\n".join(sorted(str(Path("northwind-demo") / f) for f in skeleton)))
pyproject.toml: the pom.xml
Modern Python projects declare metadata in pyproject.toml — name, version, dependencies, and build backend in one TOML file. pip install -e . then installs your own package in editable mode, so import northwind works from anywhere in the project:
from pathlib import Path
Path("northwind-demo/pyproject.toml").write_text("""\
[project]
name = "northwind-demo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["requests>=2.32", "pandas>=2.2"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
""")
print("pyproject.toml written — pip install -e . makes `import northwind` work")
# pyproject.toml written — pip install -e . makes `import northwind` work
Logging: java.util.logging's better-dressed cousin
Python's logging module mirrors what you know — loggers, levels, handlers — with one big convenience: logging.basicConfig configures a sensible default in a single call. Use module-level loggers exactly like Java's LoggerFactory.getLogger:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s: %(message)s",
)
log = logging.getLogger(__name__) # like LoggerFactory.getLogger(Class)
log.info("Fetching catalog page %d", 1)
log.warning("Rate limited — backing off %ds", 30)
# 2026-10-01 12:00:00 pipeline INFO: Fetching catalog page 1
# 2026-10-01 12:00:01 pipeline WARNING: Rate limited — backing off 30s
Key takeaways
- One project, one virtual environment (python -m venv .venv) — never install project packages globally.
- pip is Maven without the XML: pip install from PyPI, pip freeze > requirements.txt to pin.
- pytest is JUnit without boilerplate: plain functions, plain assert, fixtures over @BeforeEach.
- Standard layout: src/<package>/ + tests/ + pyproject.toml — generate it the same way every time.
- pyproject.toml is the pom.xml; pip install -e . makes your package importable.
- logging with basicConfig and module loggers covers 95% of what Java logging did for you.
You've completed the Python for Java Developers track — and the entire Python roadmap! Browse every tutorial on the Python topic page. Next on the roadmap: the Python Learning Roadmap 2026 hub post, tying all eight tracks into one guided path.
Comments
Post a Comment