Map the Java Language to Python
Your Java instincts transfer. The syntax differs, a few reflexes invert, and one or two features have no direct counterpart. This page maps the Java 21 language onto idiomatic Python, feature by feature.
Types are hints, not declarations
Java resolves types at compile time and refuses to build when they disagree. Python resolves types at runtime and reads optional type hints for tooling. You annotate for the same reasons you declare types in Java, readability and tool support, and a separate checker enforces them. Your code runs whether or not the hints agree.
def retries_left(attempts: int, limit: int = 3) -> int:
return max(limit - attempts, 0)
| Java | Python |
|---|---|
int x = 3; |
x: int = 3 (or x = 3) |
var users = ... |
users = ... (inference, no keyword) |
List<User> |
list[User] |
Map<String, Integer> |
dict[str, int] |
Optional<User> |
User | None |
Object |
typing.Any |
Run a checker to get the compile-time safety Java gives you for free. Page 1 added pyright to the dev group. mypy is the established alternative.
Records become dataclasses or pydantic models
A Java 21 record maps onto a Python dataclass, which generates the constructor, equality, and a readable repr:
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
id: int
email: str
active: bool = True
frozen=True gives you the immutability of a record. Drop it for a mutable class. When the data crosses a boundary and needs validation (a request body, a config file, a message off a queue), reach for a pydantic model instead, covered on page 4. The dataclass is your in-memory record. pydantic is your validated DTO.
Pattern matching becomes match
Java 21's pattern matching for switch maps onto Python's match statement:
match event:
case Login(user=user):
audit(user)
case Logout():
clear_session()
case _:
ignore(event)
case _ is your default. match also destructures the matched object, so it covers the record-deconstruction patterns you reach for in Java 21.
Collections and Streams
The collection types line up:
| Java | Python |
|---|---|
List, ArrayList |
list |
Map, HashMap |
dict |
Set, HashSet |
set |
| array | list or tuple |
The Stream API maps onto comprehensions. Where Java chains .stream().filter().map().collect(), Python writes one expression:
# Java: users.stream().filter(User::isActive).map(User::getEmail).toList()
emails = [u.email for u in users if u.active]
For a lazy pipeline over a large or unbounded sequence, a generator expression replaces a lazy Stream:
total = sum(line_count(f) for f in files)
Optional becomes X | None
Python represents absence with None and the type X | None. There is no Optional wrapper and no .map() chain on it. You check directly:
user = find_user(user_id)
if user is None:
raise LookupError("user not found")
greet(user)
Exceptions are all unchecked
Java splits checked and unchecked exceptions and forces you to declare or catch the checked ones. Python has one kind. Nothing forces a catch, and there is no throws clause. You raise and catch by type:
class RateLimited(Exception):
pass
try:
call_api()
except RateLimited:
backoff()
| Java | Python |
|---|---|
try / catch (E e) |
try: / except E as e: |
finally |
finally: or a context manager |
checked exception, throws |
nothing equivalent, all unchecked |
custom extends Exception |
subclass Exception |
Catch the specific type. A bare except: swallows everything, including the bugs you want to surface.
try-with-resources becomes with
Java's try-with-resources maps onto Python's with statement and context managers. Both guarantee cleanup when the block exits, whether it returns or raises:
with open("data.json") as f:
data = f.read()
# f is closed here, on success or on exception
Files, locks, database sessions, and network clients all expose context managers, the way AutoCloseable types work with try-with-resources.
Next: structure and test your code.