---
title: "Python for Java Developers"
url: "https://romankryvolapov.com/en/python-for-java-developers/"
description: "Python for those who know Java: syntax and core methods next to Java, strings, collections, generators, threads, asyncio and a microservice on the standard library."
language: en
published: 2026-11-06
updated: 2026-11-06
---
This article is for people who already know Java and want to write Python. Almost everywhere the Java code comes first, with the same thing in Python right below it, so most of the explanations are in the code comments.

The order is this: syntax first, then the core types and their methods, then what works differently from Java: variables, decorators, generators, threads and asyncio. At the end, all of this [becomes a microservice](#A-microservice-on-the-standard-library). The article covers only the language and its standard library; FastAPI, Django and Flask will get separate articles.

A few words about Python itself. Guido van Rossum created it, and the first version came out in 1991, so it is older than Java. Version 3.0 broke compatibility with version 2 in 2008, and only the third branch is alive now. The examples were run on Python 3.14 and 3.15 and on Java 26, except those needing third-party Java libraries. Where a feature is recent, its version is given.

**Pros:** far less code than in Java, a fast program start and a huge ecosystem, especially for data and machine learning.

**Cons:** computation is noticeably slower than on the JVM, threads usually do not speed it up, and without a separate tool type errors show up only at run time.

## Syntax: Java and Python side by side

### What a program looks like

In Java all code lives inside a class, and a run starts at `main`. In Python a file simply runs from top to bottom.

```java
public class Main {
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}
```

```python
# The whole file: no class, no main, no semicolon.
print("Hello")  # Hello
```

If a file can be both run and imported from another file, the startup code goes behind a check. This is the closest thing to `main` in Java.

```python
def main():
    print("Hello")  # Hello


# __name__ is the name of the current module. If the file was run
# directly, it holds "__main__". If it was imported, the file name,
# so main() is not called.
if __name__ == "__main__":
    main()
```

### How blocks and comments are written

Python uses indentation instead of curly braces. This is syntax, not style: a wrong indent stops the program from starting.

```java
int price = 150;

// A one-line comment
if (price > 100) {
    System.out.println("expensive");  // expensive
} else {
    System.out.println("cheap");
}

/* Python has no multi-line comment */
```

```python
price = 150

# A one-line comment
# No parentheses around the condition, a colon at the end,
# and the body is indented by 4 spaces.
if price > 100:
    print("expensive")  # expensive
else:
    print("cheap")
```

A long expression is wrapped inside parentheses. Instead of Javadoc, a documentation string goes right under the declaration; it is called a docstring.

```java
/**
 * Computes the total with tax.
 */
int total(int price, int tax) {
    return price
        + tax;
}

System.out.println(total(100, 20));  // 120
```

```python
def total(price, tax):
    """Computes the total with tax."""

    # A line break is allowed inside parentheses.
    return (price
            + tax)


print(total(100, 20))  # 120

# The docstring is available at run time too.
print(total.__doc__)  # Computes the total with tax.
```

The stub for an empty block in Java is `{}`. Python cannot express an empty block, so it has the word `pass`, which does nothing.

```java
void todo() {
}

class Empty {
}
```

```python
def todo():
    pass


class Empty:
    pass
```

### How to declare a variable

Python has no declaration. A variable appears on first assignment, and the type belongs not to it but to the value it holds. This is called dynamic typing. What a variable really is, is covered [below](#What-a-variable-is).

```python languages="Java, Python"
int age = 30;  # Java
age = 30  # Python

var name = "Ann";  # Java
name = "Ann"  # Python

# There is no final. UPPER_CASE means "do not change", but only by convention.
final int MAX_SIZE = 100;  # Java
MAX_SIZE = 100  # Python

# In Java this is a compile error; in Python age now holds a string.
age = "thirty"  # Python

# Assign several values at once.
int a = 1, b = 2;  # Java
a, b = 1, 2  # Python

# Swap: Java needs a temporary variable.
int tmp = a; a = b; b = tmp;  # Java
a, b = b, a  # Python

a + " " + b  # Java: 2 1
f"{a} {b}"  # Python: 2 1
```

If you do want to state the type, there is a type hint. Python does not check it, but IDEs and a separate tool, mypy, do.

```python languages="Java, Python"
int count = 0;  # Java
count: int = 0  # Python

String title = "Report";  # Java
title: str = "Report"  # Python

# Python does not object: a hint is only a note for tools.
count = "many"  # Python
```

### Naming conventions

The rules are in a document called PEP 8, the counterpart of the Java Code Conventions. The main difference is that words in variable and function names are separated by underscores.

| What | Java | Python |
|---|---|---|
| Variable | `userName` | `user_name` |
| Method, function | `findUser()` | `find_user()` |
| Class | `OrderService` | `OrderService` |
| Constant | `MAX_SIZE` | `MAX_SIZE` |
| Package | `com.shop.orders` | `orders` |
| File | `OrderService.java` | `order_service.py` |
| Private field | `private int size` | `_size` |
| Indent | 4 spaces | 4 spaces |

### What the basic types are

Python has no primitives; everything is an object, even a number. So there is no `int` and `Integer` pair. There are no different integer types either: a single `int` holds a number of any size.

| Java | Python | Example |
|---|---|---|
| `byte`, `short`, `int`, `long`, `BigInteger` | `int` | `42` |
| `float`, `double` | `float` | `3.14` |
| `boolean` | `bool` | `True`, `False` |
| `char`, `String` | `str` | `"a"`, `"Ann"` |
| `null` | `None` | `None` |
| `byte[]` | `bytes` | `b"abc"` |
| `Object` | `object` | `object()` |
| `BigDecimal` | `decimal.Decimal` | `Decimal("9.99")` |

Note that `True`, `False` and `None` start with a capital letter. There is no separate character type: a character is a string of length 1.

```python languages="Java, Python"
int number = 42;  # Java
number = 42  # Python

Object text = "Ann";  # Java
text = "Ann"  # Python

# Get the class of a value. In Python a number has one too.
text.getClass().getSimpleName()  # Java: String
type(text).__name__  # Python: str
type(number).__name__  # Python: int

# Check the type.
text instanceof String  # Java: true
isinstance(text, str)  # Python: True

# A number has methods; only a literal needs the parentheses.
Integer.bitCount(255)  # Java: 8
(255).bit_count()  # Python: 8
```

### How to convert between types

There is no cast in parentheses. You call the target type's class as a function instead.

```python languages="Java, Python"
(int) 3.7  # Java: 3
int(3.7)  # Python: 3

(double) 3  # Java: 3.0
float(3)  # Python: 3.0

Integer.parseInt("42")  # Java: 42
int("42")  # Python: 42

Double.parseDouble("3.5")  # Java: 3.5
float("3.5")  # Python: 3.5

String.valueOf(42)  # Java: 42
str(42)  # Python: 42

# A character and its code.
(int) 'A'  # Java: 65
ord("A")  # Python: 65

(char) 65  # Java: A
chr(65)  # Python: A

# A trap: in Python a non-empty string is always true, even "false".
Boolean.parseBoolean("false")  # Java: false
bool("false")  # Python: True
```

A string and a number do not join on their own. Where Java silently calls `String.valueOf`, Python raises an error.

```python languages="Java, Python"
int age = 30;  # Java
age = 30  # Python

"Age: " + age  # Java: Age: 30

# In Python "Age: " + age raises TypeError. f-strings are covered below.
"Age: " + str(age)  # Python: Age: 30
f"Age: {age}"  # Python: Age: 30
```

### What the arithmetic operators are

The most common mistake here is division. In Python `/` always gives a fractional number, and integer division is written `//`. There are no `++` and `--` operators.

```python languages="Java, Python"
7 / 2  # Java: 3
7 // 2  # Python: 3

7.0 / 2  # Java: 3.5
7 / 2  # Python: 3.5

7 % 2  # Java: 1
7 % 2  # Python: 1

# Python has an operator for powers, and the result stays whole.
Math.pow(2, 10)  # Java: 1024.0
2 ** 10  # Python: 1024

int counter = 0;  # Java
counter = 0  # Python

# counter++ does not work in Python.
counter++;  # Java
counter += 1  # Python
```

### How to compare values

Here Python is the other way around. `==` compares contents, like `equals` in Java. And `is` checks that it is the same object, like `==` on references in Java.

```python languages="Java, Python"
var first = new ArrayList<>(List.of(1, 2));  # Java
first = [1, 2]  # Python

var second = new ArrayList<>(List.of(1, 2));  # Java
second = [1, 2]  # Python

# Whether the contents are equal.
first.equals(second)  # Java: true
first == second  # Python: True

# Whether it is the same object.
first == second  # Java: false
first is second  # Python: False

# An empty value. None is checked only with is.
String name = null;  # Java
name = None  # Python

name == null  # Java: true
name is None  # Python: True

name != null  # Java: false
name is not None  # Python: False

# Comparisons can be chained, as in maths.
int age = 30;  # Java
age = 30  # Python

age >= 18 && age < 65  # Java: true
18 <= age < 65  # Python: True
```

### How to write logical conditions

Words replace `&&`, `||` and `!`. Also, a condition can be any value, not just a `boolean`: empty collections, an empty string, zero and `None` count as false.

```python languages="Java, Python"
boolean active = true;  # Java
active = True  # Python

boolean admin = false;  # Java
admin = False  # Python

active && !admin  # Java: true
active and not admin  # Python: True

active || admin  # Java: true
active or admin  # Python: True
```

```python
active = True
admin = False
print(active and not admin)  # True
print(active or admin)  # True

# The usual way to check that a collection is empty.
items = []
if not items:
    print("no items")  # no items

# One condition covers both None and empty.
name = None
if name:
    print(name)
```

`and` and `or` return not `True` or `False` but one of their operands. The short form of a default value relies on this.

```python languages="Java, Python"
String name = "";  # Java
name = ""  # Python

# or returns the first true value, here "guest".
name == null || name.isEmpty() ? "guest" : name  # Java: guest
name or "guest"  # Python: guest

# Careful: 0 is also false, so a real value gets lost.
0 or 100  # Python: 100
```

### How to write a condition and a ternary

`else if` shortens to `elif`. The ternary reads like a normal phrase: the value first, then the condition.

```java
int score = 75;
String grade;

if (score >= 90) {
    grade = "A";
} else if (score >= 60) {
    grade = "B";
} else {
    grade = "C";
}
System.out.println(grade);  // B

String result = score >= 60 ? "pass" : "fail";
System.out.println(result);  // pass
```

```python
score = 75

if score >= 90:
    grade = "A"
elif score >= 60:
    grade = "B"
else:
    grade = "C"
print(grade)  # B

# value_if_true if condition else value_if_false
result = "pass" if score >= 60 else "fail"
print(result)  # pass
```

### What replaces `switch`

Python has `match` instead, since version 3.10. Like the new `switch` in Java, it compares not only with constants but also with patterns. There is no fall-through between branches, so no `break` is needed.

```java
String httpName(int code) {
    return switch (code) {
        case 200 -> "OK";
        case 301, 302 -> "Redirect";
        case 404 -> "Not Found";
        default -> "Unknown";
    };
}

System.out.println(httpName(302));  // Redirect
System.out.println(httpName(500));  // Unknown
```

```python
def http_name(code):
    match code:
        case 200:
            return "OK"

        # The vertical bar means "or".
        case 301 | 302:
            return "Redirect"
        case 404:
            return "Not Found"

        # The underscore matches anything; this is default.
        case _:
            return "Unknown"


print(http_name(302))  # Redirect
print(http_name(500))  # Unknown
```

Matching by type works as with pattern matching in Java 21, except that a pattern can also take the value apart. Examples with `dataclass` and dictionaries come later.

```java
String describe(Object value) {
    return switch (value) {
        case Integer n when n > 100 -> "big number";
        case Integer n -> "number " + n;
        case String s -> "text " + s;
        case List<?> l -> "list of " + l.size();
        default -> "other";
    };
}

System.out.println(describe(500));  // big number
System.out.println(describe(List.of(1, 2)));  // list of 2
```

```python
def describe(value):
    match value:

        # int(n) checks the type and binds the value to n; if is when.
        case int(n) if n > 100:
            return "big number"
        case int(n):
            return f"number {n}"
        case str(s):
            return f"text {s}"
        case list(items):
            return f"list of {len(items)}"
        case _:
            return "other"


print(describe(500))  # big number
print(describe([1, 2]))  # list of 2

# And here a pattern takes a value apart: a list of exactly two items.
match ["move", 10]:
    case ["move", distance]:
        print(distance)  # 10
```

### How to write a `for` loop

Python has only one kind of `for`, the counterpart of `for (x : items)` in Java. Where Java needs a counter, you use `range`, a function that produces numbers in order.

```java
var names = List.of("Ann", "Bob");

for (String name : names) {
    System.out.print(name + " ");  // Ann Bob
}

for (int i = 0; i < 3; i++) {
    System.out.print(i + " ");  // 0 1 2
}

for (int i = 10; i > 0; i -= 3) {
    System.out.print(i + " ");  // 10 7 4 1
}
```

```python
names = ["Ann", "Bob"]

for name in names:
    print(name)  # Ann Bob

# range(3) gives 0, 1, 2. The right bound is excluded.
for i in range(3):
    print(i)  # 0 1 2

# range(from, to, step)
for i in range(10, 0, -3):
    print(i)  # 10 7 4 1
```

You need an index less often than it seems. The common cases have ready-made functions, and `for i in range(len(names))` is considered bad style.

```java
var names = List.of("Ann", "Bob");
var ages = List.of(30, 25);

// A value and its index
for (int i = 0; i < names.size(); i++) {
    System.out.print(i + ":" + names.get(i) + " ");  // 0:Ann 1:Bob
}

// Two lists in parallel
for (int i = 0; i < names.size(); i++) {
    System.out.print(names.get(i) + "=" + ages.get(i) + " ");  // Ann=30 Bob=25
}

// In reverse order
for (int i = names.size() - 1; i >= 0; i--) {
    System.out.print(names.get(i) + " ");  // Bob Ann
}
```

```python
names = ["Ann", "Bob"]
ages = [30, 25]

# enumerate yields (index, value) pairs.
for i, name in enumerate(names):
    print(f"{i}:{name}")  # 0:Ann 1:Bob

# zip walks two lists at once and yields pairs.
for name, age in zip(names, ages):
    print(f"{name}={age}")  # Ann=30 Bob=25

for name in reversed(names):
    print(name)  # Bob Ann
```

### How to write `while` and leave a loop

`while`, `break` and `continue` work as in Java. There is no `do-while` loop and no labels for leaving nested loops.

```java
int n = 0;
while (n < 10) {
    n++;
    if (n % 2 == 0) {
        continue;
    }
    if (n > 5) {
        break;
    }
    System.out.print(n + " ");  // 1 3 5
}

int tries = 0;
do {
    tries++;
} while (tries < 3);
System.out.println(tries);  // 3
```

```python
n = 0
while n < 10:
    n += 1
    if n % 2 == 0:
        continue
    if n > 5:
        break
    print(n)  # 1 3 5

# do-while is written as an endless loop with a check at the end.
tries = 0
while True:
    tries += 1
    if tries >= 3:
        break
print(tries)  # 3
```

A loop can have an `else` branch, which Java lacks. It runs if the loop reached the end without leaving through `break`. It usually replaces a `found` flag.

```java
var ids = List.of(1, 2, 3);
boolean found = false;

for (int id : ids) {
    if (id == 7) {
        found = true;
        break;
    }
}
if (!found) {
    System.out.println("not found");  // not found
}
```

```python
ids = [1, 2, 3]

for id_ in ids:
    if id_ == 7:
        break
else:

    # We get here only if break did not fire.
    print("not found")  # not found
```

The `:=` operator assigns a value right inside an expression. In Java an assignment is already an expression; Python needs a separate operator for it.

```java
var names = List.of("Ann", "Bob");
int count;

if ((count = names.size()) > 1) {
    System.out.println(count);  // 2
}
```

```python
names = ["Ann", "Bob"]

# Counted and checked at once. A plain = here would be a SyntaxError.
if (count := len(names)) > 1:
    print(count)  # 2
```

### How to declare a function

In Java a method always lives in a class. In Python a function can live right in a file, so classes of static methods are not needed here. Parameter and result types can be given as hints, but that is optional.

```java
int add(int a, int b) {
    return a + b;
}

void greet(String name) {
    System.out.println("Hi, " + name);  // Hi, Ann
}

System.out.println(add(2, 3));  // 5
greet("Ann");
```

```python
def add(a, b):
    return a + b


# The same with type hints. None here means void.
def greet(name: str) -> None:
    print(f"Hi, {name}")  # Hi, Ann


print(add(2, 3))  # 5
greet("Ann")

# A function with no return actually returns None.
print(greet("Ann"))  # None
```

### What replaces method overloading

Python has no overloading: a second `def` with the same name simply replaces the first. Default parameter values are used instead. Arguments can be named at the call, which also replaces the Builder pattern.

```java
String connect(String host, int port, boolean secure) {
    return host + ":" + port + " " + secure;
}

String connect(String host, int port) {
    return connect(host, port, false);
}

String connect(String host) {
    return connect(host, 80);
}

System.out.println(connect("a"));  // a:80 false
System.out.println(connect("a", 443, true));  // a:443 true
```

```python
def connect(host, port=80, secure=False):
    return f"{host}:{port} {secure}"


print(connect("a"))  # a:80 False

# By position, as in Java.
print(connect("a", 443, True))  # a:443 True

# By name: port is skipped, and it is clear what True means.
print(connect("a", secure=True))  # a:80 True
```

### How to pass any number of arguments

`int... numbers` from Java is written `*numbers`. Python also has what Java lacks: `**options` collects named arguments into a dictionary.

```java
int sum(int... numbers) {
    return Arrays.stream(numbers).sum();
}

System.out.println(sum(1, 2, 3));  // 6

int[] values = {1, 2, 3};
System.out.println(sum(values));  // 6
```

```python
# numbers receives a tuple (1, 2, 3). A tuple is an immutable list.
def total(*numbers):
    return sum(numbers)


print(total(1, 2, 3))  # 6

# At the call, the star does the opposite: it spreads a list into arguments.
values = [1, 2, 3]
print(total(*values))  # 6


# options receives a dictionary {"port": 80, "secure": True}.
def connect(host, **options):
    print(host, options)  # a {'port': 80, 'secure': True}


connect("a", port=80, secure=True)
```

A star in the parameter list can forbid passing by position. Everything after it can be passed only by name. This is done for flags, to avoid calls such as `send(msg, True, False)`.

```python
def send(message, *, retry=False):
    return f"{message} {retry}"


print(send("hi", retry=True))  # hi True

# TypeError: this way of passing is forbidden.
# send("hi", True)
```

### How to return several values

In Java this takes a separate class or a `record`. In Python a function simply returns several values separated by commas, and they get packed into a tuple.

```java
record MinMax(int min, int max) {
}

MinMax minMax(List<Integer> items) {
    return new MinMax(Collections.min(items), Collections.max(items));
}

var result = minMax(List.of(3, 1, 2));
System.out.println(result.min() + " " + result.max());  // 1 3
```

```python
def min_max(items):
    return min(items), max(items)


# The tuple is unpacked straight into two variables.
low, high = min_max([3, 1, 2])
print(low, high)  # 1 3

# An unneeded value traditionally goes into a variable named _ .
low, _ = min_max([3, 1, 2])

# The star takes "everything else".
head, *rest = [1, 2, 3]
print(head, rest)  # 1 [2, 3]
```

### How to write a lambda and a method reference

A function in Python is an ordinary object: it can be stored in a variable and passed to another function. So functional interfaces such as `Function` and `Comparator` are not needed, and instead of `.apply()` you just call the function. A lambda can hold only one expression.

```python languages="Java, Python"
Function<Integer, Integer> twice = x -> x * 2;  # Java
twice = lambda x: x * 2  # Python

twice.apply(4)  # Java: 8
twice(4)  # Python: 8

# A method reference is just the function's name without parentheses.
Function<String, Integer> size = String::length;  # Java
size = len  # Python

size.apply("Ann")  # Java: 3
size("Ann")  # Python: 3

# Several parameters.
BinaryOperator<Integer> plus = (a, b) -> a + b;  # Java
plus = lambda a, b: a + b  # Python

plus.apply(2, 3)  # Java: 5
plus(2, 3)  # Python: 5
```

The most common use of a lambda is a sort key. Instead of a `Comparator` that compares two objects, Python takes a function that returns, for one object, what to sort by.

```python languages="Java, Python"
var names = new ArrayList<>(List.of("Bob", "Al", "Cindy"));  # Java
names = ["Bob", "Al", "Cindy"]  # Python

names.sort(Comparator.comparingInt(String::length));  # Java
names.sort(key=len)  # Python

names  # Java: [Al, Bob, Cindy]
names  # Python: ['Al', 'Bob', 'Cindy']

names.sort(Comparator.comparing(s -> s.charAt(s.length() - 1)));  # Java
names.sort(key=lambda s: s[-1])  # Python

names  # Java: [Bob, Al, Cindy]
names  # Python: ['Bob', 'Al', 'Cindy']
```

### How to declare a class

The constructor is called `__init__`. Fields are not declared in advance: they appear when the constructor assigns to them. There is no `new`; an object is created by calling the class. Every method's first parameter is `self`. It is the same as `this` in Java, only written explicitly.

```java
class User {
    private String name;
    private int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    String greet() {
        return "Hi, " + this.name;
    }
}

User user = new User("Ann", 30);
System.out.println(user.greet());  // Hi, Ann
```

```python
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # You do not pass self at the call: Python supplies it.
    def greet(self):
        return f"Hi, {self.name}"


user = User("Ann", 30)
print(user.greet())  # Hi, Ann

# Exactly the same, with self passed by hand.
print(User.greet(user))  # Hi, Ann
```

A method with double underscores on both sides, such as `__init__`, is called a dunder method (short for double underscore). You do not call it directly: Python itself calls it when it needs to create, print or compare an object. There will be many of these below.

### How to make static fields and methods

A variable declared in the class body belongs to the class, like a `static` field in Java. Methods have two decorators. `@staticmethod` is a plain static method. `@classmethod` receives the class itself as its first parameter and is the usual way to write factory methods. A decorator is a mark above a function, similar to a Java annotation. How it works is explained [below](#What-a-decorator-is).

```java
class Money {
    static final String CURRENCY = "USD";
    int cents;

    Money(int cents) {
        this.cents = cents;
    }

    static boolean isValid(int cents) {
        return cents >= 0;
    }

    static Money ofDollars(int dollars) {
        return new Money(dollars * 100);
    }
}

System.out.println(Money.CURRENCY);  // USD
System.out.println(Money.isValid(-1));  // false
System.out.println(Money.ofDollars(2).cents);  // 200
```

```python
class Money:
    CURRENCY = "USD"

    def __init__(self, cents):
        self.cents = cents

    @staticmethod
    def is_valid(cents):
        return cents >= 0

    # cls is the class Money itself. In a subclass it is the subclass.
    @classmethod
    def of_dollars(cls, dollars):
        return cls(dollars * 100)


print(Money.CURRENCY)  # USD
print(Money.is_valid(-1))  # False
print(Money.of_dollars(2).cents)  # 200
```

### How to make a field private and what replaces getters

Python has no access modifiers. A single leading underscore means "this is internal, do not touch it from outside", but nothing forbids it; this is a convention.

| Java | Python | What it means |
|---|---|---|
| `public int size` | `size` | public |
| `protected int size` | `_size` | internal, by convention |
| `private int size` | `_size` | the same; there is no separate level |

Getters and setters are not written in advance; the field is simply public. If a check is needed later, the field is replaced with `@property`: from outside the code stays `account.balance`, but a method runs.

```java
class Account {
    private int balance;

    int getBalance() {
        return balance;
    }

    void setBalance(int balance) {
        if (balance < 0) {
            throw new IllegalArgumentException("negative");
        }
        this.balance = balance;
    }
}

var account = new Account();
account.setBalance(100);
System.out.println(account.getBalance());  // 100
```

```python
class Account:
    def __init__(self):
        self._balance = 0

    # The getter: read as a field, account.balance, no parentheses.
    @property
    def balance(self):
        return self._balance

    # The setter: fires on account.balance = 100.
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("negative")
        self._balance = value


account = Account()
account.balance = 100
print(account.balance)  # 100
```

### What replaces `toString`, `equals` and `hashCode`

These are dunder methods too. There is a difference from Java: there are two string forms. `__str__` is for users and is what `print` uses. `__repr__` is for developers, and it is what debuggers and containers show. If only `__repr__` is declared, it also serves as `__str__`, so that is the one usually written.

```java
class Point {
    int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Point p && p.x == x && p.y == y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

System.out.println(new Point(1, 2));  // Point(1, 2)
System.out.println(new Point(1, 2).equals(new Point(1, 2)));  // true
```

```python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    # NotImplemented means "I cannot compare with this type".
    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):
        return hash((self.x, self.y))


print(Point(1, 2))  # Point(1, 2)
print(Point(1, 2) == Point(1, 2))  # True
```

### What replaces `record`

Hardly anyone writes the previous class by hand: there is `dataclass` for it. This decorator generates `__init__`, `__repr__` and `__eq__` from a list of fields. With `frozen=True` the object becomes immutable, like a `record`, and gets `__hash__`.

```java
record Point(int x, int y) {
    public Point {
        if (x < 0) {
            throw new IllegalArgumentException("x");
        }
    }

    Point withX(int newX) {
        return new Point(newX, y);
    }
}

var point = new Point(1, 2);
System.out.println(point);  // Point[x=1, y=2]
System.out.println(point.x());  // 1
System.out.println(point.withX(5));  // Point[x=5, y=2]
```

```python
from dataclasses import dataclass, field, replace


# Type hints are required here: the decorator finds the fields by them.
@dataclass(frozen=True)
class Point:
    x: int
    y: int = 0

    # The record's compact constructor: runs after __init__.
    def __post_init__(self):
        if self.x < 0:
            raise ValueError("x")


point = Point(1, 2)
print(point)  # Point(x=1, y=2)
print(point.x)  # 1

# replace makes a copy with a changed field, like withX.
print(replace(point, x=5))  # Point(x=5, y=2)

# point.x = 5 would raise FrozenInstanceError.


# A mutable default goes through field: each object gets its own list.
@dataclass
class Cart:
    items: list = field(default_factory=list)


print(Cart())  # Cart(items=[])
```

### How to inherit from a class

The parent goes in parentheses. Calling the parent constructor is optional and does not happen by itself, so `super().__init__()` is easy to forget. All methods can be overridden, and there is no `@Override`. A class can have several parents; more on this [below](#How-multiple-inheritance-works).

```java
class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    String speak() {
        return name + " makes a sound";
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    String speak() {
        return super.speak() + ": woof";
    }
}

Animal pet = new Dog("Rex");
System.out.println(pet.speak());  // Rex makes a sound: woof
System.out.println(pet instanceof Animal);  // true
```

```python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} makes a sound"


class Dog(Animal):
    def __init__(self, name):
        super().__init__(name)

    def speak(self):
        return f"{super().speak()}: woof"


pet = Dog("Rex")
print(pet.speak())  # Rex makes a sound: woof
print(isinstance(pet, Animal))  # True
```

### What replaces an abstract class and an interface

An abstract class is declared through `ABC`. It cannot be instantiated, and a subclass must implement the abstract methods. The difference from Java is that the error appears not at compile time but when the object is created.

```java
abstract class Shape {
    abstract double area();

    String describe() {
        return "area " + area();
    }
}

class Square extends Shape {
    double side = 2;

    @Override
    double area() {
        return side * side;
    }
}

System.out.println(new Square().describe());  // area 4.0
```

```python
from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    def describe(self):
        return f"area {self.area()}"


class Square(Shape):
    side = 2.0

    def area(self):
        return self.side * self.side


print(Square().describe())  # area 4.0

# Shape() would raise TypeError: an abstract class cannot be created.
```

The closest thing to an interface is `Protocol`. It differs in one important way: a class does not write `implements`. Any class with the same methods fits. This is called duck typing: if an object can do what is needed, its type does not matter. It is mypy, not Python, that checks the fit.

```java
interface Notifier {
    String send(String text);
}

class Email implements Notifier {
    public String send(String text) {
        return "email: " + text;
    }
}

String alert(Notifier notifier) {
    return notifier.send("down");
}

System.out.println(alert(new Email()));  // email: down
```

```python
from typing import Protocol


class Notifier(Protocol):
    def send(self, text: str) -> str:
        ...


# No implements, no inheritance: matching methods are enough.
class Email:
    def send(self, text: str) -> str:
        return f"email: {text}"


def alert(notifier: Notifier) -> str:
    return notifier.send("down")


print(alert(Email()))  # email: down
```

### How to declare an `enum`

An enumeration is a class that inherits from `Enum`. As in Java, each element exists in one instance, so they can be compared with `is`. If the values are strings, `StrEnum` is handy: its elements are strings.

```java
enum Status {
    NEW("new"),
    PAID("paid");

    final String code;

    Status(String code) {
        this.code = code;
    }
}

System.out.println(Status.PAID.name());  // PAID
System.out.println(Status.PAID.code);  // paid
System.out.println(Status.valueOf("NEW").code);  // new
System.out.println(Status.values().length);  // 2
```

```python
from enum import Enum, StrEnum


class Status(Enum):
    NEW = "new"
    PAID = "paid"


print(Status.PAID.name)  # PAID
print(Status.PAID.value)  # paid

# Look up by name and by value.
print(Status["NEW"].value)  # new
print(Status("paid").name)  # PAID
print(len(Status))  # 2
print(Status.PAID is Status("paid"))  # True


# A StrEnum element equals a string, handy for JSON and databases.
class Color(StrEnum):
    RED = "red"


print(Color.RED == "red")  # True
```

### What replaces anonymous and nested classes

There are no anonymous classes, and usually no need for them, since you can pass a function. Nested classes exist, but they are like a `static class` in Java: they have no access to an outer object.

```java
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("run");  // run
    }
};
task.run();
```

```python
def run_task(task):
    task()


# Instead of an anonymous class, a function declared where it is needed.
def task():
    print("run")  # run


run_task(task)
```

### How to write a generic

The type hint of a collection goes in square brackets. Since Python 3.12 your own generic classes are declared as in Java, only with different brackets. As in Java, nothing is left of the type parameter at run time. More in the section on [type hints](#Type-hints).

```python languages="Java, Python"
List<String> names = new ArrayList<>();  # Java
names: list[str] = []  # Python

Map<String, Integer> ages = new HashMap<>();  # Java
ages: dict[str, int] = {}  # Python

# A value or null.
String nickname = null;  # Java
nickname: str | None = None  # Python
```

```java
class Box<T> {
    private final T value;

    Box(T value) {
        this.value = value;
    }

    T get() {
        return value;
    }
}

<T extends Comparable<T>> T maxOf(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

System.out.println(new Box<>("x").get());  // x
System.out.println(maxOf(1, 2));  // 2
```

```python
class Box[T]:
    def __init__(self, value: T) -> None:
        self._value = value

    def get(self) -> T:
        return self._value


# T: int | str is a bound, like T extends in Java.
def max_of[T: int | str](a: T, b: T) -> T:
    return a if a > b else b


print(Box("x").get())  # x
print(max_of(1, 2))  # 2
```

### How to handle exceptions

`catch` is called `except`, and `throw` is called `raise`. There are no checked exceptions: the word `throws` does not exist, and nothing has to be caught.

```java
int parse(String text) {
    try {
        return Integer.parseInt(text);
    } catch (NumberFormatException | NullPointerException e) {
        System.out.println("bad: " + text);  // bad: abc
        return 0;
    } finally {
        System.out.println("done");  // done
    }
}

parse("abc");
```

```python
def parse(text):
    try:
        return int(text)

    # Several types are listed in parentheses.
    except (ValueError, TypeError) as e:
        print(f"bad: {text}")  # bad: abc
        return 0
    finally:
        print("done")  # done


parse("abc")
```

Your own exception is declared as a class. The cause, as in Java's `cause`, is given with `from`. There is also an `else` branch: it runs only if `try` raised no error.

```java
class OrderException extends RuntimeException {
    OrderException(String message, Throwable cause) {
        super(message, cause);
    }
}

try {
    try {
        Integer.parseInt("abc");
    } catch (NumberFormatException e) {
        throw new OrderException("bad id", e);
    }
} catch (OrderException e) {
    System.out.println(e.getMessage());  // bad id
    System.out.println(e.getCause() instanceof NumberFormatException);  // true
}
```

```python
class OrderError(Exception):
    pass


try:
    try:
        int("abc")
    except ValueError as e:
        raise OrderError("bad id") from e
except OrderError as e:
    print(e)  # bad id

    # __cause__ is the same as getCause().
    print(isinstance(e.__cause__, ValueError))  # True

try:
    value = int("42")
except ValueError:
    print("bad")
else:

    # Only if there was no error.
    print(value)  # 42
```

The standard exceptions have other names, but the mapping is simple:

| Java | Python |
|---|---|
| `Throwable` | `BaseException` |
| `Exception` | `Exception` |
| `IllegalArgumentException`, `NumberFormatException` | `ValueError` |
| `ClassCastException` | `TypeError` |
| `NullPointerException` | `AttributeError` or `TypeError` |
| `IndexOutOfBoundsException` | `IndexError` |
| missing key in a `Map` | `KeyError` |
| `IOException` | `OSError` |
| `FileNotFoundException` | `FileNotFoundError` |
| `ArithmeticException` | `ZeroDivisionError` |
| `UnsupportedOperationException` | `NotImplementedError` |
| `StackOverflowError` | `RecursionError` |
| `TimeoutException` | `TimeoutError` |

### What replaces try-with-resources

Python has the `with` statement instead. An object for it is called a context manager. Instead of `close` from `AutoCloseable` it has two methods: `__enter__` runs on the way in, and `__exit__` on the way out, even after an error. The numbers show the order of execution.

```java
class Resource implements AutoCloseable {
    Resource() {
        System.out.println("open");  // 1. open
    }

    @Override
    public void close() {
        System.out.println("close");  // 3. close
    }
}

try (var resource = new Resource()) {
    System.out.println("work");  // 2. work
}
```

```python
class Resource:
    def __enter__(self):
        print("open")  # 1. open

        # What __enter__ returns goes into the variable after as.
        return self

    def __exit__(self, exc_type, exc, tb):
        print("close")  # 3. close


with Resource() as resource:
    print("work")  # 2. work
```

You meet `with` most often when working with files:

```java
Path path = Files.createTempFile("demo", ".txt");

try (var writer = Files.newBufferedWriter(path)) {
    writer.write("hello");
}

System.out.println(Files.readString(path));  // hello
```

```python
import tempfile
from pathlib import Path

path = Path(tempfile.mkdtemp()) / "demo.txt"

# The file closes by itself. Always give the encoding, or the system one is used.
with open(path, "w", encoding="utf-8") as file:
    file.write("hello")

print(path.read_text(encoding="utf-8"))  # hello
```

### How imports and packages work

Python has no `package` line: a file is itself a module, named after the file. A directory of modules is called a package. One file can hold any number of classes and functions, and a separate file for every class is not the custom here. You can import either a module or a single name from it.

```python languages="Java, Python"
# Importing a single name.
import java.time.LocalDate;  # Java
from datetime import date  # Python

# A static import.
import static java.lang.Math.sqrt;  # Java
from math import sqrt  # Python

# Importing everything. This is frowned upon in Python.
import java.util.*;  # Java
from math import *  # Python

# Python only: the whole module, then access through its name.
import math  # Python
math.floor(2.5)  # Python: 2

# Python only: your own short name.
import datetime as dt  # Python
dt.date(2026, 1, 2).year  # Python: 2026
```

```text
orders/                 a package, like package orders
├── __init__.py         marks a package, usually empty
├── domain.py           the module orders.domain
└── api/
    ├── __init__.py
    └── routes.py       the module orders.api.routes
```

```python
# From another file in the project.
from orders.domain import Order

# From a sibling file in the same package: the dot means "this package".
from .domain import Order

# One level up.
from ..domain import Order
```

### Syntax cheat sheet

| Java | Python |
|---|---|
| `int a = 1;` | `a = 1` |
| `final int MAX = 1;` | `MAX = 1` |
| `null` | `None` |
| `true`, `false` | `True`, `False` |
| `a.equals(b)` | `a == b` |
| `a == b` for references | `a is b` |
| `&&`, `\|\|`, `!` | `and`, `or`, `not` |
| `x > 0 ? a : b` | `a if x > 0 else b` |
| `7 / 2` | `7 // 2` |
| `Math.pow(2, 3)` | `2 ** 3` |
| `i++` | `i += 1` |
| `else if` | `elif` |
| `switch` | `match` |
| `for (int i = 0; i < n; i++)` | `for i in range(n):` |
| `for (T x : items)` | `for x in items:` |
| `do { } while (x);` | `while True:` plus `break` |
| `void f(int x)` | `def f(x):` |
| `int... xs` | `*xs` |
| `x -> x * 2` | `lambda x: x * 2` |
| `String::length` | `len` |
| `new User("Ann")` | `User("Ann")` |
| `this` | `self` |
| `class B extends A` | `class B(A):` |
| `super.run()` | `super().run()` |
| `static int f()` | `@staticmethod` |
| `record Point(int x)` | `@dataclass(frozen=True)` |
| `interface` | `Protocol` |
| `abstract class` | `class A(ABC):` |
| `List<String>` | `list[str]` |
| `throw` | `raise` |
| `catch (E e)` | `except E as e:` |
| `try (var r = ...)` | `with ... as r:` |
| `toString()` | `__repr__`, `__str__` |
| `equals()`, `hashCode()` | `__eq__`, `__hash__` |
| `System.out.println(x)` | `print(x)` |

## Numbers: core operations

### How integers differ from Java

`int` in Python does not overflow; it grows as long as there is memory. Division with a minus sign works differently too: Java drops the fractional part, while Python rounds down.

```python languages="Java, Python"
Integer.MAX_VALUE + 1  # Java: -2147483648
2147483647 + 1  # Python: 2147483648

BigInteger.TWO.pow(70)  # Java: 1180591620717411303424
2 ** 70  # Python: 1180591620717411303424

# Division with a minus: the results differ.
-7 / 2  # Java: -3
-7 // 2  # Python: -4

-7 % 3  # Java: -1
-7 % 3  # Python: 2

# Underscores in numbers, binary and hex literals are as in Java.
1_000_000  # Java: 1000000
1_000_000  # Python: 1000000

0xFF  # Java: 255
0xFF  # Python: 255

0b101  # Java: 5
0b101  # Python: 5

# Converting to another base. Python adds a prefix.
Integer.toBinaryString(10)  # Java: 1010
bin(10)  # Python: 0b1010

Integer.toHexString(255)  # Java: ff
hex(255)  # Python: 0xff

Integer.parseInt("ff", 16)  # Java: 255
int("ff", 16)  # Python: 255

# Bit operations are the same, except >>>: a number has no fixed length.
5 & 3  # Java: 1
5 & 3  # Python: 1

1 << 4  # Java: 16
1 << 4  # Python: 16
```

### Where to find `Math`

The most common functions are built into the language and need no import; the rest live in the `math` module.

```python languages="Java, Python"
import math  # Python

Math.abs(-5)  # Java: 5
abs(-5)  # Python: 5

Math.max(1, 2)  # Java: 2
max(1, 2)  # Python: 2

# min and max in Python take both several values and a list.
Collections.min(List.of(3, 1))  # Java: 1
min(3, 1)  # Python: 1

# floor and ceil in Python return an integer, not a float.
Math.floor(2.7)  # Java: 2.0
math.floor(2.7)  # Python: 2

Math.ceil(2.1)  # Java: 3.0
math.ceil(2.1)  # Python: 3

Math.sqrt(16)  # Java: 4.0
math.sqrt(16)  # Python: 4.0

Math.PI  # Java: 3.141592653589793
math.pi  # Python: 3.141592653589793

# A trap: round in Python rounds a half to the even number.
Math.round(2.5)  # Java: 3
round(2.5)  # Python: 2

Math.round(3.5)  # Java: 4
round(3.5)  # Python: 4

# Round to two decimal places.
Math.round(3.14159 * 100) / 100.0  # Java: 3.14
round(3.14159, 2)  # Python: 3.14

# Quotient and remainder at once.
divmod(7, 2)  # Python: (3, 1)

Double.isNaN(0.0 / 0)  # Java: true
math.isnan(float("nan"))  # Python: True

# In Java dividing a float by zero gives Infinity; in Python, ZeroDivisionError.
1.0 / 0  # Java: Infinity
float("inf")  # Python: inf
```

### How to do money arithmetic

`float` in Python is the same `double`, with the same rounding errors. For money there is `Decimal`, the counterpart of `BigDecimal`. Create it from a string. The usual operators work, so `add` and `multiply` are not needed.

```python languages="Java, Python"
from decimal import Decimal, ROUND_HALF_UP  # Python

0.1 + 0.2  # Java: 0.30000000000000004
0.1 + 0.2  # Python: 0.30000000000000004

new BigDecimal("0.1").add(new BigDecimal("0.2"))  # Java: 0.3
Decimal("0.1") + Decimal("0.2")  # Python: 0.3

new BigDecimal("19.99").multiply(new BigDecimal("3"))  # Java: 59.97
Decimal("19.99") * 3  # Python: 59.97

# Round to cents.
new BigDecimal("2.675").setScale(2, RoundingMode.HALF_UP)  # Java: 2.68
Decimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_UP)  # Python: 2.68

# Comparison: Java's equals counts the scale; Python's == only the value.
new BigDecimal("1.0").equals(new BigDecimal("1.00"))  # Java: false
Decimal("1.0") == Decimal("1.00")  # Python: True
```

### How to get a random number

Note that `randint` includes both bounds. The `random` module is not suitable for passwords and tokens; there is `secrets` for those, like `SecureRandom` in Java.

```python languages="Java, Python"
import random  # Python
import secrets  # Python

# A die: from 1 to 6.
int dice = ThreadLocalRandom.current().nextInt(1, 7);  # Java
dice = random.randint(1, 6)  # Python

# A float from 0 to 1.
double share = Math.random();  # Java
share = random.random()  # Python

# A random element and a shuffle.
var cards = new ArrayList<>(List.of(1, 2, 3));  # Java
cards = [1, 2, 3]  # Python

Collections.shuffle(cards);  # Java
random.shuffle(cards)  # Python

int card = cards.get(new Random().nextInt(cards.size()));  # Java
card = random.choice(cards)  # Python

# A security token.
var token = new java.security.SecureRandom().nextLong();  # Java
token = secrets.token_hex(16)  # Python
```

## Strings: core methods

### How to write a string

Like `String` in Java, `str` is immutable and holds Unicode text. Single and double quotes mean the same thing. Besides the plain string there are several kinds of literals: a letter before the quote changes the meaning.

| Literal | What it is | Java counterpart |
|---|---|---|
| `"text"`, `'text'` | a plain string | `"text"` |
| `"""text"""` | a multi-line string | `"""text"""` |
| `f"{name}"` | a string with substituted values | `String.format` |
| `r"\d+"` | a raw string: the backslash does not escape | none |
| `b"abc"` | bytes, not text | `byte[]` |
| `t"{name}"` | a template: values apart from the text (3.14) | none |

The letters can be combined: `rf"\d{count}"` or `rb"\x00"`.

```python languages="Java, Python"
"it's"  # Java: it's
'it\'s'  # Python: it's

# Handy when the text holds quotes of the other kind.
"say \"hi\""  # Java: say "hi"
'say "hi"'  # Python: say "hi"

# A raw string saves backslashes in regular expressions.
"\\d+"  # Java: \d+
r"\d+"  # Python: \d+

# A Unicode character by its code.
"é"  # Java: é
"é"  # Python: é
```

A multi-line string is written like a Java text block, but Python does not strip the indentation itself. `textwrap.dedent` does that.

```java
String query = """
    SELECT *
    FROM users
    """;
System.out.print(query);
```

```python
from textwrap import dedent

# The backslash after the quotes removes the first line break.
query = dedent("""\
    SELECT *
    FROM users
    """)
print(query)
```

Adjacent literals hide a trap. Python joins them with no plus at all. This is handy for long strings, but if you forget a comma in a list, there is no error.

```python
message = (
    "first part, "
    "second part"
)
print(message)  # first part, second part

# A forgotten comma: the list has two items, not three.
names = [
    "Ann",
    "Bob"
    "Cid",
]
print(names)  # ['Ann', 'BobCid']
```

### What methods a string has

These are the methods you need every day. A string is immutable, so all of them, as in Java, return a new string.

```python languages="Java, Python"
String s = "Hello World";  # Java
s = "Hello World"  # Python

# Length is a function, not a method.
s.length()  # Java: 11
len(s)  # Python: 11

s.charAt(0)  # Java: H
s[0]  # Python: H

# A negative index counts from the end.
s.charAt(s.length() - 1)  # Java: d
s[-1]  # Python: d

s.toUpperCase()  # Java: HELLO WORLD
s.upper()  # Python: HELLO WORLD

s.toLowerCase()  # Java: hello world
s.lower()  # Python: hello world

s.contains("World")  # Java: true
"World" in s  # Python: True

s.startsWith("He")  # Java: true
s.startswith("He")  # Python: True

s.endsWith("ld")  # Java: true
s.endswith("ld")  # Python: True

s.indexOf("o")  # Java: 4
s.find("o")  # Python: 4

s.lastIndexOf("o")  # Java: 7
s.rfind("o")  # Python: 7

# Not found: -1. The s.index() method raises ValueError here.
s.indexOf("z")  # Java: -1
s.find("z")  # Python: -1

s.replace("World", "Ann")  # Java: Hello Ann
s.replace("World", "Ann")  # Python: Hello Ann

# Replace only the first occurrence.
s.replaceFirst("l", "L")  # Java: HeLlo World
s.replace("l", "L", 1)  # Python: HeLlo World

# How many times it occurs. Java has no ready method.
s.chars().filter(c -> c == 'l').count()  # Java: 3
s.count("l")  # Python: 3
```

```python languages="Java, Python"
# Whitespace at the edges.
"  a  ".strip()  # Java: a
"  a  ".strip()  # Python: a

"  a".stripLeading()  # Java: a
"  a".lstrip()  # Python: a

"a  ".stripTrailing()  # Java: a
"a  ".rstrip()  # Python: a

# In Python strip can also remove given characters.
"--a--".strip("-")  # Python: a

# Remove a prefix, only if it is there.
"v1.2".removeprefix("v")  # Python: 1.2

"ab".repeat(2)  # Java: abab
"ab" * 2  # Python: abab

# Reverse a string.
new StringBuilder("abc").reverse()  # Java: cba
"abc"[::-1]  # Python: cba

# Whether a string is empty.
"".isEmpty()  # Java: true
not ""  # Python: True

"  ".isBlank()  # Java: true
not "  ".strip()  # Python: True

# Character checks. In Python they work for a whole string too.
Character.isDigit('5')  # Java: true
"5".isdigit()  # Python: True

Character.isLetter('a')  # Java: true
"a".isalpha()  # Python: True

"123".chars().allMatch(Character::isDigit)  # Java: true
"123".isdigit()  # Python: True

# A capital first letter.
"ann".substring(0, 1).toUpperCase() + "ann".substring(1)  # Java: Ann
"ann".capitalize()  # Python: Ann

# Comparison: ordinary operators replace compareTo in Python.
"a".compareTo("b") < 0  # Java: true
"a" < "b"  # Python: True

"ANN".equalsIgnoreCase("ann")  # Java: true
"ANN".casefold() == "ann".casefold()  # Python: True
```

### What a string slice is

A slice replaces `substring`. It is written `s[start:end:step]`. The right bound is excluded, as in `substring`. Any part can be omitted. It helps to picture the indexes as numbers under the letters. Slices work the same way on lists.

```text
 string:     H   e   l   l   o
 index:     0   1   2   3   4
 from end:   -5  -4  -3  -2  -1
```

```python languages="Java, Python"
String s = "Hello";  # Java
s = "Hello"  # Python

s.substring(1, 3)  # Java: el
s[1:3]  # Python: el

# No start: from the first character.
s.substring(0, 2)  # Java: He
s[:2]  # Python: He

# No end: to the last character.
s.substring(2)  # Java: llo
s[2:]  # Python: llo

# The last three characters.
s.substring(s.length() - 3)  # Java: llo
s[-3:]  # Python: llo

# Every second character. Java needs a loop for this.
s[::2]  # Python: Hlo

# Unlike substring, going out of bounds is not an error.
s[3:100]  # Python: lo
```

### How to split and join strings

This is where most differences are. `split` in Java takes a regular expression and drops trailing empty strings, while `split` in Python drops nothing and looks for plain text. With no arguments it splits on any whitespace. In Python `join` is called on the separator.

```python languages="Java, Python"
Arrays.toString("a,b,,".split(","))  # Java: [a, b]
"a,b,,".split(",")  # Python: ['a', 'b', '', '']

# In Java the dot is a regex; in Python, a plain character.
Arrays.toString("a.b".split("\\."))  # Java: [a, b]
"a.b".split(".")  # Python: ['a', 'b']

Arrays.toString(" a  b ".trim().split("\\s+"))  # Java: [a, b]
" a  b ".split()  # Python: ['a', 'b']

# Limit the number of parts. Python counts the cuts, not the parts.
Arrays.toString("a=b=c".split("=", 2))  # Java: [a, b=c]
"a=b=c".split("=", 1)  # Python: ['a', 'b=c']

# By lines.
"a\nb".lines().toList()  # Java: [a, b]
"a\nb".splitlines()  # Python: ['a', 'b']

# Split into three parts: before, separator, after.
"key=value".partition("=")  # Python: ('key', '=', 'value')

String.join(", ", List.of("a", "b"))  # Java: a, b
", ".join(["a", "b"])  # Python: a, b

# In Python join takes only strings; numbers must be converted.
", ".join(str(n) for n in [1, 2])  # Python: 1, 2

# Iterate over characters and get a list of them.
"abc".toCharArray().length  # Java: 3
list("abc")  # Python: ['a', 'b', 'c']
```

### How to put values into a string

The main way is an f-string: the letter `f` before the quote and any expressions in curly braces. After a colon comes the format, much like the one in `String.format`.

```python languages="Java, Python"
String name = "Ann";  # Java
name = "Ann"  # Python

int n = 42;  # Java
n = 42  # Python

double x = 3.14159;  # Java
x = 3.14159  # Python

String.format("%s is %d", name, n)  # Java: Ann is 42
f"{name} is {n}"  # Python: Ann is 42

# Any expression can go in the braces.
String.format("%d", n * 2)  # Java: 84
f"{n * 2}"  # Python: 84

# Decimal places.
String.format(Locale.US, "%.2f", x)  # Java: 3.14
f"{x:.2f}"  # Python: 3.14

# Width and zeros.
String.format("%05d", n)  # Java: 00042
f"{n:05d}"  # Python: 00042

# Alignment: > right, < left, ^ centre.
String.format("[%5s]", name)  # Java: [  Ann]
f"[{name:>5}]"  # Python: [  Ann]

String.format("[%-5s]", name)  # Java: [Ann  ]
f"[{name:<5}]"  # Python: [Ann  ]

f"[{name:^5}]"  # Python: [ Ann ]

# A thousands separator.
String.format(Locale.US, "%,d", 1234567)  # Java: 1,234,567
f"{1234567:,}"  # Python: 1,234,567

# Hex and percent.
String.format("%x", 255)  # Java: ff
f"{255:x}"  # Python: ff

f"{0.25:.0%}"  # Python: 25%

# Python only: = prints both the expression and its value. Handy for debugging.
f"{n=}"  # Python: n=42

# A literal curly brace is written twice.
f"{{{n}}}"  # Python: {42}
```

Older code has two more ways, worth knowing in order to read it. The `%` format also lives on in logging: `log.info("user %s", name)`. This way the message is built only if the record actually reaches the log, as with `{}` in SLF4J. That is why f-strings are not used in logger calls.

```python
name = "Ann"

# Like printf. The oldest way.
print("Hi, %s" % name)  # Hi, Ann

# Like MessageFormat. Needed when the template is given in advance, say in settings.
print("Hi, {}".format(name))  # Hi, Ann
print("Hi, {who}".format(who=name))  # Hi, Ann
```

### What a t-string is

An f-string glues everything into plain text at once, so putting user data into SQL or HTML this way is dangerous: that is how injections happen. The t-string arrived in Python 3.14. It glues nothing and hands over the text and values separately, so you decide how to combine them.

```python
name = "x' OR '1'='1"

# An f-string: the data became part of the SQL.
print(f"name = '{name}'")  # name = 'x' OR '1'='1'

# A t-string: an object, not a string.
template = t"name = {name}"
print(template.strings)  # ('name = ', '')
print(template.values)  # ("x' OR '1'='1",)


# The text goes into the query and the values apart, as in PreparedStatement.
def to_sql(query):
    return "?".join(query.strings), query.values


print(to_sql(template))  # ('name = ?', ("x' OR '1'='1",))
```

### How to build a string from parts

A string is immutable, so concatenating in a loop creates a new string each time. As in Java, this is slow at volume. Python has no separate `StringBuilder`: parts go into a list and get joined once with `join`.

```java
var builder = new StringBuilder();
for (int i = 0; i < 3; i++) {
    builder.append(i).append(",");
}
System.out.println(builder);  // 0,1,2,
```

```python
parts = []
for i in range(3):
    parts.append(f"{i},")
print("".join(parts))  # 0,1,2,

# If text is built as a stream, there is io.StringIO, the StringWriter.
import io

buffer = io.StringIO()
buffer.write("a")
buffer.write("b")
print(buffer.getvalue())  # ab
```

### How `__str__` differs from `__repr__` when printing

The difference shows well on a string itself. The `str` function gives the text as is, while `repr` shows how the value is written in code: with quotes and control characters. Containers print their elements through `repr`, which is why strings in a list appear with quotes.

```python
text = "a\tb"

print(str(text))  # a b
print(repr(text))  # 'a\tb'

# In an f-string, !r means "call repr". Handy in error messages.
print(f"bad value: {text!r}")  # bad value: 'a\tb'
print(["a", "b"])  # ['a', 'b']
```

### How text differs from bytes

`str` holds text and `bytes` holds bytes. Python never mixes them: they cannot be added, and going from one to the other is always explicit, through an encoding. There are three types for working with bytes.

| Python | Mutable | What it is | Java counterpart |
|---|---|---|---|
| `bytes` | no | immutable bytes | a `byte[]` nobody changes |
| `bytearray` | yes | a mutable buffer | `byte[]` |
| `memoryview` | as its source | a window into other bytes, with no copy | `ByteBuffer.wrap` |

```python languages="Java, Python"
import base64  # Python

byte[] data = "héllo".getBytes(StandardCharsets.UTF_8);  # Java
data = "héllo".encode("utf-8")  # Python

new String(data, StandardCharsets.UTF_8)  # Java: héllo
data.decode("utf-8")  # Python: héllo

# Length in bytes: é takes two.
data.length  # Java: 6
len(data)  # Python: 6

# A byte: signed in Java, 0 to 255 in Python.
data[1]  # Java: -61
data[1]  # Python: 195

HexFormat.of().formatHex(data)  # Java: 68c3a96c6c6f
data.hex()  # Python: 68c3a96c6c6f

Base64.getEncoder().encodeToString("hi".getBytes())  # Java: aGk=
base64.b64encode(b"hi").decode()  # Python: aGk=
```

```python
data = b"abc"

# Printed with the letter b, so text and bytes do not get confused.
print(data)  # b'abc'

# "abc" + b"abc" raises TypeError.

# bytes cannot be changed; bytearray can.
buffer = bytearray(b"abc")
buffer[0] = 65
print(buffer)  # bytearray(b'Abc')

# memoryview looks at the same bytes: a write shows in the source.
view = memoryview(buffer)
view[1:2] = b"B"
print(buffer)  # bytearray(b'ABc')
```

### How string length differs from Java

`String` in Java counts UTF-16 units, so an emoji takes two `char`s. In Python `len` counts Unicode characters. Python itself chooses how many bytes to give a character, so indexing is always fast. But one visible character can still consist of several Unicode characters, and such text is normalized before comparing.

```python languages="Java, Python"
import unicodedata  # Python

"😀".length()  # Java: 2
len("😀")  # Python: 1

"😀".codePointCount(0, 2)  # Java: 1
len("😀")  # Python: 1

# é as one character and as e plus an accent mark.
"é".equals("é")  # Java: false
"é" == "é"  # Python: False

Normalizer.normalize("é", Normalizer.Form.NFC).equals("é")  # Java: true
unicodedata.normalize("NFC", "é") == "é"  # Python: True
```

### How to work with regular expressions

The expression syntax is almost the same, and the methods live in the `re` module. A pattern is usually written as a raw string. With no match, the methods return `None`.

```python languages="Java, Python"
import re  # Python

# Whether the whole string matches.
"123".matches("\\d+")  # Java: true
bool(re.fullmatch(r"\d+", "123"))  # Python: True

"a1b2".replaceAll("\\d", "#")  # Java: a#b#
re.sub(r"\d", "#", "a1b2")  # Python: a#b#

Arrays.toString("a1b2c".split("\\d"))  # Java: [a, b, c]
re.split(r"\d", "a1b2c")  # Python: ['a', 'b', 'c']

# All matches.
var found = Pattern.compile("\\d").matcher("a1b2").results();  # Java
found.map(r -> r.group()).toList()  # Java: [1, 2]
re.findall(r"\d", "a1b2")  # Python: ['1', '2']
```

```java
Pattern pattern = Pattern.compile("(?<key>\\w+)=(?<value>\\d+)");
Matcher matcher = pattern.matcher("size=42");

if (matcher.find()) {
    System.out.println(matcher.group("key"));  // size
    System.out.println(matcher.group("value"));  // 42
}
```

```python
import re

# A precompiled pattern, like Pattern. A group name: ?P<name>.
pattern = re.compile(r"(?P<key>\w+)=(?P<value>\d+)")

# search looks anywhere in the string, like find().
if match := pattern.search("size=42"):
    print(match["key"])  # size
    print(match["value"])  # 42
```

## Collections: core methods

### Which collection corresponds to which

The main collections are built into the language and have their own syntax, so they need no `new` and no imports. There is no `List` interface with several implementations either: there is one `list`, and inside it is a dynamic array, like `ArrayList`.

| Java | Python | Literal |
|---|---|---|
| `ArrayList` | `list` | `[1, 2]` |
| `List.of` | `tuple` | `(1, 2)` |
| `LinkedHashMap` | `dict` | `{"a": 1}` |
| `HashSet` | `set` | `{1, 2}` |
| `Set.of` | `frozenset` | `frozenset({1, 2})` |
| `ArrayDeque`, `LinkedList` | `collections.deque` | `deque([1, 2])` |
| `PriorityQueue` | `heapq` | functions over a list |
| `int[]` | `list` or `array.array` | `[0] * 3` |
| `TreeMap` | none, usually `sorted(d)` | |
| `ConcurrentHashMap` | `dict` plus `Lock` | |

### How to work with a list

```python languages="Java, Python"
var items = new ArrayList<>(List.of(3, 1, 2));  # Java
items = [3, 1, 2]  # Python

items.size()  # Java: 3
len(items)  # Python: 3

items.get(0)  # Java: 3
items[0]  # Python: 3

# The last element.
items.getLast()  # Java: 2
items[-1]  # Python: 2

items.set(0, 9);  # Java
items[0] = 9  # Python

items.add(4);  # Java
items.append(4)  # Python

# Insert at an index.
items.add(0, 7);  # Java
items.insert(0, 7)  # Python

items.addAll(List.of(5, 6));  # Java
items.extend([5, 6])  # Python

items  # Java: [7, 9, 1, 2, 4, 5, 6]
items  # Python: [7, 9, 1, 2, 4, 5, 6]

# Remove at an index and get the value.
items.remove(0)  # Java: 7
items.pop(0)  # Python: 7

# Remove by value.
items.remove(Integer.valueOf(9));  # Java
items.remove(9)  # Python

items.contains(1)  # Java: true
1 in items  # Python: True

# If the value is missing, index raises ValueError, not -1.
items.indexOf(2)  # Java: 1
items.index(2)  # Python: 1

items.isEmpty()  # Java: false
not items  # Python: False

Collections.max(items)  # Java: 6
max(items)  # Python: 6

items.stream().mapToInt(i -> i).sum()  # Java: 18
sum(items)  # Python: 18

# Reverse in place.
Collections.reverse(items);  # Java
items.reverse()  # Python

items  # Java: [6, 5, 4, 2, 1]
items  # Python: [6, 5, 4, 2, 1]

# Part of a list. In Java it is a view of the list; in Python, a copy.
items.subList(0, 2)  # Java: [6, 5]
items[:2]  # Python: [6, 5]

# A copy of the whole list.
var copy = new ArrayList<>(items);  # Java
copy = items.copy()  # Python

# Joining and repeating.
[1] + [2]  # Python: [1, 2]
[0] * 3  # Python: [0, 0, 0]

items.clear();  # Java
items.clear()  # Python
```

A slice also works on the left of an assignment, and multiplication hides a trap with nested lists.

```python
items = [1, 2, 3, 4]

# Replace part of a list.
items[1:3] = [20, 30]
print(items)  # [1, 20, 30, 4]

# Delete part of a list.
del items[1:3]
print(items)  # [1, 4]

# A trap: two references to the same inner list.
grid = [[0] * 2] * 2
grid[0][0] = 1
print(grid)  # [[1, 0], [1, 0]]

# Correct: each inner list is created separately.
grid = [[0] * 2 for _ in range(2)]
grid[0][0] = 1
print(grid)  # [[1, 0], [0, 0]]
```

### What a tuple is

A `tuple` is an immutable list, the closest thing to `List.of`. It is used for a fixed set of values, such as coordinates or a database row. It can also be a dictionary key, unlike a list.

```python languages="Java, Python"
var point = List.of(10, 20);  # Java
point = (10, 20)  # Python

point.get(0)  # Java: 10
point[0]  # Python: 10

# point.set(0, 1) in Java and point[0] = 1 in Python both fail.

# A one-element tuple: the comma is required.
var single = List.of(1);  # Java
single = (1,)  # Python

# Without the comma it is just a number in parentheses.
type((1)).__name__  # Python: int
type((1,)).__name__  # Python: tuple

# Unpacking into variables.
var x = point.get(0);  # Java
x, y = point  # Python

x  # Java: 10
y  # Python: 20

# Tuples compare element by element.
(1, 2) < (1, 3)  # Python: True
```

### How to work with a dictionary

A `dict` is a `Map`. It keeps insertion order, like `LinkedHashMap`. The key difference from Java is that `ages["Bob"]` with a missing key raises `KeyError` rather than returning `null`. Java's `get` behaviour comes from the `get` method.

```python languages="Java, Python"
var ages = new LinkedHashMap<String, Integer>();  # Java
ages = {}  # Python

ages.put("Ann", 30);  # Java
ages["Ann"] = 30  # Python

ages.put("Bob", 25);  # Java
ages["Bob"] = 25  # Python

ages.get("Ann")  # Java: 30
ages["Ann"]  # Python: 30

# No such key.
ages.get("Cid")  # Java: null
ages.get("Cid")  # Python: None

ages.getOrDefault("Cid", 0)  # Java: 0
ages.get("Cid", 0)  # Python: 0

ages.containsKey("Ann")  # Java: true
"Ann" in ages  # Python: True

ages.containsValue(25)  # Java: true
25 in ages.values()  # Python: True

ages.size()  # Java: 2
len(ages)  # Python: 2

ages.keySet()  # Java: [Ann, Bob]
list(ages)  # Python: ['Ann', 'Bob']

ages.values()  # Java: [30, 25]
list(ages.values())  # Python: [30, 25]

# Write only if the key is not there yet.
ages.putIfAbsent("Cid", 40);  # Java
ages.setdefault("Cid", 40)  # Python: 40

# Remove and get the value.
ages.remove("Cid")  # Java: 40
ages.pop("Cid")  # Python: 40

# Remove, and if the key is missing, return a default.
ages.pop("Cid", None)  # Python: None

# Keys in order, instead of TreeMap.
new TreeMap<>(ages).firstKey()  # Java: Ann
sorted(ages)[0]  # Python: Ann
```

Iteration goes through `items`, which yields key and value pairs. It replaces `entrySet`.

```java
var ages = new LinkedHashMap<>(Map.of("Ann", 30));

for (var entry : ages.entrySet()) {
    System.out.print(entry.getKey() + "=" + entry.getValue());  // Ann=30
}

for (String name : ages.keySet()) {
    System.out.print(name);  // Ann
}

ages.forEach((name, age) -> System.out.print(name + age));
```

```python
ages = {"Ann": 30}

for name, age in ages.items():
    print(f"{name}={age}")  # Ann=30

# Iterating over the dict itself goes over the keys.
for name in ages:
    print(name)  # Ann

# Merging: on equal keys, the right one wins.
print({"a": 1} | {"a": 2, "b": 3})  # {'a': 2, 'b': 3}

# Two stars spread one dictionary inside another.
print({**{"a": 1}, "b": 2})  # {'a': 1, 'b': 2}
```

A key can only be an immutable value: a string, a number, a tuple, a frozen `dataclass`. Formally it needs a `__hash__` method, the same as `hashCode`. A list would raise `TypeError: unhashable type`.

### How to accumulate values per key

Java has `computeIfAbsent` and `merge` for this. Python has `setdefault` and two special dictionaries. `defaultdict` creates the value for a new key by itself. `Counter` counts how many times each element occurs.

```java
var byCity = new TreeMap<String, List<String>>();
byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Ann");
byCity.computeIfAbsent("Kyiv", k -> new ArrayList<>()).add("Bob");
System.out.println(byCity);  // {Kyiv=[Ann, Bob]}

var counts = new TreeMap<String, Integer>();
for (String word : List.of("a", "b", "a")) {
    counts.merge(word, 1, Integer::sum);
}
System.out.println(counts);  // {a=2, b=1}
```

```python
from collections import Counter, defaultdict

# list here is a function that creates the value for a new key.
by_city = defaultdict(list)
by_city["Kyiv"].append("Ann")
by_city["Kyiv"].append("Bob")
print(dict(by_city))  # {'Kyiv': ['Ann', 'Bob']}

counts = Counter(["a", "b", "a"])
print(dict(counts))  # {'a': 2, 'b': 1}
print(counts["a"])  # 2

# The most common elements: top 1.
print(counts.most_common(1))  # [('a', 2)]
```

### How to work with a set

The methods resemble Java's, and ordinary operators replace `retainAll` and `removeAll`. They return a new set and do not change the originals.

```python languages="Java, Python"
var seen = new TreeSet<>(List.of(1, 2));  # Java
seen = {1, 2}  # Python

seen.add(3);  # Java
seen.add(3)  # Python

seen.contains(3)  # Java: true
3 in seen  # Python: True

seen.remove(3);  # Java
seen.remove(3)  # Python

# In Python remove raises KeyError if the value is missing; discard stays quiet.
seen.discard(100)  # Python

# An empty set: {} would create a dictionary.
var empty = new HashSet<Integer>();  # Java
empty = set()  # Python

# Union, intersection, difference.
{1, 2} | {2, 3}  # Python: {1, 2, 3}
{1, 2} & {2, 3}  # Python: {2}
{1, 2} - {2, 3}  # Python: {1}

# Whether all elements are in, like containsAll.
seen.containsAll(List.of(1))  # Java: true
{1} <= seen  # Python: True
```

### How to make a queue and a priority queue

`list.pop(0)` takes O(n) because it shifts the whole array, so a queue uses `deque`. The priority queue is not a class but functions from the `heapq` module. They work on a plain list, and the smallest element is always on top.

```python languages="Java, Python"
from collections import deque  # Python
import heapq  # Python

var queue = new ArrayDeque<>(List.of(1, 2));  # Java
queue = deque([1, 2])  # Python

queue.addLast(3);  # Java
queue.append(3)  # Python

queue.addFirst(0);  # Java
queue.appendleft(0)  # Python

queue.pollFirst()  # Java: 0
queue.popleft()  # Python: 0

queue.pollLast()  # Java: 3
queue.pop()  # Python: 3

# Peek without removing.
queue.peekFirst()  # Java: 1
queue[0]  # Python: 1

# A priority queue.
var tasks = new PriorityQueue<>(List.of(3, 1));  # Java
tasks = [1, 3]  # Python

tasks.add(2);  # Java
heapq.heappush(tasks, 2)  # Python

tasks.poll()  # Java: 1
heapq.heappop(tasks)  # Python: 1

# With a priority: a tuple compares by its first element.
jobs = [(2, "later"), (1, "urgent")]; heapq.heapify(jobs)  # Python
heapq.heappop(jobs)  # Python: (1, 'urgent')

# The N largest at once.
heapq.nlargest(2, [1, 5, 3])  # Python: [5, 3]
```

### How to sort

`list.sort()` changes the list itself, while `sorted()` returns a new one and takes any collection. The ordering comes from the `key` function. To sort by several fields it simply returns a tuple.

```java
record User(String name, int age) {
}

var users = new ArrayList<>(List.of(
    new User("Ann", 30),
    new User("Bob", 25),
    new User("Cid", 30)
));

// By age descending, then by name.
users.sort(
    Comparator.comparingInt(User::age).reversed()
        .thenComparing(User::name)
);

System.out.println(users.getFirst().name());  // Ann
```

```python
from dataclasses import dataclass


@dataclass
class User:
    name: str
    age: int


users = [
    User("Ann", 30),
    User("Bob", 25),
    User("Cid", 30),
]

# By age descending, then by name. The minus reverses the order for numbers.
users.sort(key=lambda u: (-u.age, u.name))
print(users[0].name)  # Ann

# sorted does not change the original, and reverse=True flips the order.
print(sorted([2, 3, 1], reverse=True))  # [3, 2, 1]
print(sorted("bca"))  # ['a', 'b', 'c']
```

### What happens if you change a collection while iterating

In Java this raises `ConcurrentModificationException`. A dictionary and a set in Python behave the same way, but a list silently skips elements.

```python
items = [1, 2, 2, 3]

# No error, yet the second 2 remains.
for item in items:
    if item == 2:
        items.remove(item)
print(items)  # [1, 2, 3]

# Correct: build a new list. The syntax is explained in the next section.
items = [1, 2, 2, 3]
items = [item for item in items if item != 2]
print(items)  # [1, 3]

ages = {"a": 1}
try:
    for key in ages:
        ages["b"] = 2
except RuntimeError as e:
    print(e)  # dictionary changed size during iteration
```

### What the complexity of the operations is

The values are the same as for the corresponding Java collections.

| Operation | `list` | `dict`, `set` | `deque` |
|---|---|---|---|
| Read by index or key | O(1) | O(1) | O(n) |
| `x in c` | O(n) | O(1) | O(n) |
| Append | O(1) | O(1) | O(1) |
| Add or remove at the front | O(n) | – | O(1) |
| `sorted` | O(n log n) | O(n log n) | O(n log n) |

## Stream API: what replaces it

### What a list comprehension is

Instead of a chain of `filter` and `map`, Python has a special notation called a list comprehension. It reads like a phrase: what to put, where to take it from, under what condition. It replaces most of the Stream API.

```python
numbers = [1, 2, 3, 4]

# [ what to put   where to take it   condition ]
squares = [n * n for n in numbers if n % 2 == 0]
print(squares)  # [4, 16]

# The same as a plain loop.
squares = []
for n in numbers:
    if n % 2 == 0:
        squares.append(n * n)
print(squares)  # [4, 16]
```

### What replaces stream operations

Here are the main operations with their results. Note that the combination of `map` and `filter` is a single expression, with no separate calls.

```python languages="Java, Python"
import functools  # Python

var items = List.of(3, 1, 2, 3);  # Java
items = [3, 1, 2, 3]  # Python

# filter
items.stream().filter(i -> i > 1).toList()  # Java: [3, 2, 3]
[i for i in items if i > 1]  # Python: [3, 2, 3]

# map
items.stream().map(i -> i * 2).toList()  # Java: [6, 2, 4, 6]
[i * 2 for i in items]  # Python: [6, 2, 4, 6]

# sum, count, min, max
items.stream().mapToInt(i -> i).sum()  # Java: 9
sum(items)  # Python: 9

items.stream().filter(i -> i > 1).count()  # Java: 3
sum(1 for i in items if i > 1)  # Python: 3

items.stream().max(Integer::compare).get()  # Java: 3
max(items)  # Python: 3

# The average.
items.stream().mapToInt(i -> i).average().getAsDouble()  # Java: 2.25
sum(items) / len(items)  # Python: 2.25

# anyMatch, allMatch, noneMatch
items.stream().anyMatch(i -> i > 2)  # Java: true
any(i > 2 for i in items)  # Python: True

items.stream().allMatch(i -> i > 2)  # Java: false
all(i > 2 for i in items)  # Python: False

items.stream().noneMatch(i -> i > 5)  # Java: true
not any(i > 5 for i in items)  # Python: True

# findFirst
items.stream().filter(i -> i < 3).findFirst().orElse(null)  # Java: 1
next((i for i in items if i < 3), None)  # Python: 1

# sorted, limit, skip
items.stream().sorted().toList()  # Java: [1, 2, 3, 3]
sorted(items)  # Python: [1, 2, 3, 3]

items.stream().limit(2).toList()  # Java: [3, 1]
items[:2]  # Python: [3, 1]

items.stream().skip(2).toList()  # Java: [2, 3]
items[2:]  # Python: [2, 3]

# distinct keeping the order: dictionary keys are unique.
items.stream().distinct().toList()  # Java: [3, 1, 2]
list(dict.fromkeys(items))  # Python: [3, 1, 2]

# Without order, a set is enough.
items.stream().collect(Collectors.toCollection(TreeSet::new))  # Java: [1, 2, 3]
{i for i in items}  # Python: {1, 2, 3}

# flatMap
Stream.of(List.of(1), List.of(2)).flatMap(List::stream).toList()  # Java: [1, 2]
[x for row in [[1], [2]] for x in row]  # Python: [1, 2]

# joining
var words = items.stream().map(String::valueOf);  # Java
words.collect(Collectors.joining(","))  # Java: 3,1,2,3
",".join(str(i) for i in items)  # Python: 3,1,2,3

# reduce lives in functools.
items.stream().reduce(1, (a, b) -> a * b)  # Java: 18
functools.reduce(lambda a, b: a * b, items)  # Python: 18
```

Parentheses instead of square brackets in expressions like `sum(1 for i in items if i > 1)` create a generator. It builds no list in memory and yields values one at a time, like a lazy stream. More on this [below](#Iterators-and-generators).

### How to build a dictionary and group values

There are comprehensions for dictionaries and sets too. But `groupingBy` has no simple counterpart: grouping is usually written as a loop.

```java
record User(String name, String city, int age) {
}

var users = List.of(
    new User("Ann", "Kyiv", 30),
    new User("Bob", "Lviv", 25),
    new User("Cid", "Kyiv", 40)
);

var agesByName = users.stream()
    .collect(Collectors.toMap(User::name, User::age));
System.out.println(agesByName.get("Ann"));  // 30

var byCity = users.stream()
    .collect(Collectors.groupingBy(User::city));
System.out.println(byCity.get("Kyiv").size());  // 2

var countByCity = users.stream()
    .collect(Collectors.groupingBy(User::city, Collectors.counting()));
System.out.println(countByCity.get("Kyiv"));  // 2

var adults = users.stream()
    .collect(Collectors.partitioningBy(u -> u.age() >= 30));
System.out.println(adults.get(true).size());  // 2
```

```python
from collections import Counter, defaultdict
from dataclasses import dataclass


@dataclass
class User:
    name: str
    city: str
    age: int


users = [
    User("Ann", "Kyiv", 30),
    User("Bob", "Lviv", 25),
    User("Cid", "Kyiv", 40),
]

# toMap
ages_by_name = {u.name: u.age for u in users}
print(ages_by_name["Ann"])  # 30

# groupingBy
by_city = defaultdict(list)
for user in users:
    by_city[user.city].append(user)
print(len(by_city["Kyiv"]))  # 2

# groupingBy + counting
count_by_city = Counter(u.city for u in users)
print(count_by_city["Kyiv"])  # 2

# partitioningBy
adults = [u for u in users if u.age >= 30]
print(len(adults))  # 2
```

### What replaces `Optional`

Python does not use `Optional`. A missing value is `None`, and the type is written as `User | None`. Chains give way to ordinary checks.

```java
record User(String name) {
}

Optional<User> findUser(int id) {
    return id == 1 ? Optional.of(new User("Ann")) : Optional.empty();
}

String name = findUser(2)
    .map(User::name)
    .orElse("guest");
System.out.println(name);  // guest
```

```python
from dataclasses import dataclass


@dataclass
class User:
    name: str


def find_user(user_id: int) -> User | None:
    return User("Ann") if user_id == 1 else None


user = find_user(2)
name = user.name if user is not None else "guest"
print(name)  # guest
```

## Frequently needed standard library classes

### How to work with date and time

The `datetime` module replaces `java.time`. One important detail: a `datetime` may or may not have a time zone. Without one it is like `LocalDateTime`; with one, like `ZonedDateTime`. These cannot be compared with each other, and `datetime.now()` with no arguments gives a value without a zone. In services always write `datetime.now(UTC)`.

```python languages="Java, Python"
from datetime import UTC, date, datetime, timedelta  # Python
from zoneinfo import ZoneInfo  # Python

import java.time.format.DateTimeFormatter;  # Java
import java.time.*;  # Java

var day = LocalDate.of(2026, 11, 6);  # Java
day = date(2026, 11, 6)  # Python

# Add and subtract: ordinary operators replace plusDays.
day.plusDays(30)  # Java: 2026-12-06
day + timedelta(days=30)  # Python: 2026-12-06

day.getDayOfWeek().getValue()  # Java: 5
day.isoweekday()  # Python: 5

# The difference between dates.
Duration.ofHours(36).toDays()  # Java: 1
timedelta(hours=36).days  # Python: 1

# To a string and back.
day.toString()  # Java: 2026-11-06
day.isoformat()  # Python: 2026-11-06

LocalDate.parse("2026-01-02").getYear()  # Java: 2026
date.fromisoformat("2026-01-02").year  # Python: 2026

day.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))  # Java: 06.11.2026
day.strftime("%d.%m.%Y")  # Python: 06.11.2026

# The current moment with a zone.
var now = ZonedDateTime.now(ZoneOffset.UTC);  # Java
now = datetime.now(UTC)  # Python

var kyiv = ZoneId.of("Europe/Kyiv");  # Java
var at = ZonedDateTime.of(2026, 1, 2, 12, 0, 0, 0, kyiv);  # Java
at = datetime(2026, 1, 2, 12, tzinfo=ZoneInfo("Europe/Kyiv"))  # Python

at.toEpochSecond()  # Java: 1767348000
int(at.timestamp())  # Python: 1767348000
```

### How to work with files

`Path` and `Files` are combined in one Python class, `pathlib.Path`. Path parts are joined with the `/` operator.

```python languages="Java, Python"
import tempfile  # Python
from pathlib import Path  # Python

Path dir = Files.createTempDirectory("demo");  # Java
dir = Path(tempfile.mkdtemp())  # Python

Path file = dir.resolve("logs").resolve("a.txt");  # Java
file = dir / "logs" / "a.txt"  # Python

file.getFileName()  # Java: a.txt
file.name  # Python: a.txt

file.getParent().getFileName()  # Java: logs
file.parent.name  # Python: logs

Files.exists(file)  # Java: false
file.exists()  # Python: False

# Create directories with their parents.
Files.createDirectories(file.getParent());  # Java
file.parent.mkdir(parents=True, exist_ok=True)  # Python

Files.writeString(file, "one\ntwo\n");  # Java
file.write_text("one\ntwo\n", encoding="utf-8")  # Python

Files.readAllLines(file)  # Java: [one, two]
file.read_text(encoding="utf-8").splitlines()  # Python: ['one', 'two']

# Walk a directory with all its subdirectories.
Files.walk(dir).filter(Files::isRegularFile).count()  # Java: 1
sum(1 for f in dir.rglob("*.txt"))  # Python: 1

Files.delete(file);  # Java
file.unlink()  # Python
```

A large file is read line by line. A file object in Python can be iterated in a loop, and only one line at a time goes into memory.

```java
Path path = Files.createTempFile("big", ".txt");
Files.writeString(path, "1\n2\n");

try (var lines = Files.lines(path)) {
    System.out.println(lines.mapToInt(Integer::parseInt).sum());  // 3
}
```

```python
import tempfile
from pathlib import Path

path = Path(tempfile.mkdtemp()) / "big.txt"
path.write_text("1\n2\n", encoding="utf-8")

with open(path, encoding="utf-8") as file:
    print(sum(int(line) for line in file))  # 3
```

### How to work with JSON

The `json` module is in the standard library. Unlike Jackson, it works only with dictionaries and lists and does not map to your classes. Real projects use Pydantic for that. A `dataclass` object can be turned into a dictionary with `asdict`.

```java
ObjectMapper mapper = new ObjectMapper();

String text = mapper.writeValueAsString(new User("Ann", 30));
User user = mapper.readValue(text, User.class);
Map<String, Object> raw = mapper.readValue(text, Map.class);
```

```python
import json
from dataclasses import asdict, dataclass


@dataclass
class User:
    name: str
    age: int


# An object to JSON: first a dictionary, then a string.
text = json.dumps(asdict(User("Ann", 30)))
print(text)  # {"name": "Ann", "age": 30}

# JSON to a dictionary.
raw = json.loads(text)
print(raw["name"])  # Ann

# A dictionary to an object: two stars spread it into named arguments.
print(User(**raw))  # User(name='Ann', age=30)

# json does not know types like datetime and Decimal; it needs a default function.
from decimal import Decimal

print(json.dumps({"sum": Decimal("1.5")}, default=str))  # {"sum": "1.5"}
```

### How to write logs

The `logging` module is SLF4J and Logback rolled into one. A logger is created in each module and configured once at startup. The `__name__` variable holds the current module's name. Messages use `%s`, like `{}` in SLF4J.

```java
private static final Logger log = LoggerFactory.getLogger(OrderService.class);

log.info("order {} paid", id);

try {
    pay(id);
} catch (Exception e) {
    log.error("payment failed", e);
}
```

```python
import logging

log = logging.getLogger(__name__)

# Setup at program start, instead of logback.xml.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

log.info("order %s paid", 42)

try:
    1 / 0
except ZeroDivisionError:

    # exception writes the message and the whole stack, like log.error("...", e).
    log.exception("payment failed")
```

### Where to find the rest

```python languages="Java, Python"
import hashlib  # Python
import os  # Python
import sys  # Python
import time  # Python
import uuid  # Python

# An environment variable or a default.
String port = System.getenv().getOrDefault("NO_SUCH_VAR", "80");  # Java
port = os.environ.get("NO_SUCH_VAR", "80")  # Python

# Command-line arguments. sys.argv[0] is the file name.
String[] args = new String[0];  # Java
args = sys.argv[1:]  # Python

# Measure time.
long start = System.nanoTime();  # Java
start = time.perf_counter()  # Python

Thread.sleep(1);  # Java
time.sleep(0.001)  # Python

# A unique identifier.
UUID.randomUUID().toString().length()  # Java: 36
len(str(uuid.uuid4()))  # Python: 36

# The first 8 characters of a SHA-256 hash.
var sha = java.security.MessageDigest.getInstance("SHA-256");  # Java
HexFormat.of().formatHex(sha.digest(new byte[0])).substring(0, 8)  # Java: e3b0c442
hashlib.sha256(b"").hexdigest()[:8]  # Python: e3b0c442
```

| Java | Python |
|---|---|
| `System.exit(1)` | `sys.exit(1)` |
| `System.err.println(x)` | `print(x, file=sys.stderr)` |
| `new Scanner(System.in)` | `input()` |
| `HttpClient` | `urllib.request`, in practice `httpx` |
| `URLEncoder.encode` | `urllib.parse.quote` |
| `Properties` | `configparser`, `tomllib` |
| `@Cacheable` | `functools.lru_cache` |
| `Thread`, `ExecutorService` | `threading`, `concurrent.futures` |
| `CompletableFuture` | `asyncio` |
| `JUnit` | `unittest`, in practice `pytest` |

## How Python works differently from Java

### How code runs

The main implementation of the language is called CPython. Like `javac` with the JVM, it compiles source to bytecode and runs it on a virtual machine, except that both steps are one command. The bytecode is cached in the `__pycache__` directory, like `.class` files. The main difference from the JVM is that the bytecode is usually interpreted. The JIT compiler, which on the JVM turns hot code into machine code, is only just arriving in CPython and is still experimental. That explains the difference in speed.

| | Java | Python |
|---|---|---|
| Compilation | `javac` in advance | at start |
| A type error shows | at compile time | when the line runs |
| JIT | C2, Graal | experimental |
| Program start | slower | faster |
| Garbage collection | tracing | reference counts plus a cycle collector |
| Parallel threads | yes | usually no, because of the GIL |

Declaring a class or a function in Python is an ordinary command that runs when its turn comes. In Java the JVM loads classes; here the file simply goes from top to bottom. The numbers show the order.

```python
print("start")  # 1. start


# Here the function is only created. The body does not run.
def greet():
    print("hi")  # 4. hi


# But a class body runs immediately.
class Config:
    print("class body")  # 2. class body


print("end")  # 3. end
greet()
```

This explains a lot of what follows. Importing a file also just runs it from top to bottom, so side-effect code at the top level of a module fires on first import. That is why the `if __name__ == "__main__"` check is needed.

### What a variable is

All variables in Python work like reference variables in Java. There are no primitives, so a variable always holds a reference to an object, even for a number. The assignment `b = a` copies only the reference. It helps to picture a variable as a label stuck on an object.

```text
a = [1, 2]        a ──► [1, 2]

b = a             a ──► [1, 2] ◄── b

b.append(3)       a ──► [1, 2, 3] ◄── b

b = [9]           a ──► [1, 2, 3]
                  b ──► [9]
```

```java
var a = new ArrayList<>(List.of(1, 2));
var b = a;
b.add(3);
System.out.println(a);  // [1, 2, 3]

b = new ArrayList<>(List.of(9));
System.out.println(a);  // [1, 2, 3]
```

```python
a = [1, 2]
b = a
b.append(3)
print(a)  # [1, 2, 3]

# The label b moved to another object. The object a did not change.
b = [9]
print(a)  # [1, 2, 3]

# id() is the object's address, like System.identityHashCode.
c = a
print(id(c) == id(a))  # True
```

### How arguments are passed to a function

The same way as objects in Java: a function receives a copy of the reference. It can change the object's contents, but it cannot replace the caller's variable.

```java
void change(List<Integer> items) {
    items.add(1);
    items = new ArrayList<>();
    items.add(99);
}

var data = new ArrayList<Integer>();
change(data);
System.out.println(data);  // [1]
```

```python
def change(items):

    # 1. Changing the shared object: visible outside.
    items.append(1)

    # 2. Moving the local label: not visible outside.
    items = []
    items.append(99)


data = []
change(data)
print(data)  # [1]
```

### Which types are mutable and which are not

| Immutable | Mutable |
|---|---|
| `int`, `float`, `bool`, `None` | `list` |
| `str`, `bytes` | `dict` |
| `tuple`, `frozenset` | `set`, `bytearray` |
| `@dataclass(frozen=True)` | ordinary objects |

This leads to an important trap with `+=`. On a mutable object it changes the object itself; on an immutable one it creates a new object and moves the label.

```python
# A list: one object for both, the change shows through both labels.
a = [1]
b = a
a += [2]
print(b)  # [1, 2]

# A string: a now points to a new string, and b to the old one.
a = "x"
b = a
a += "y"
print(b)  # x
```

### How to copy an object

`list(x)` or `x.copy()` is a shallow copy, like `new ArrayList<>(x)`: the list itself is copied, while nested objects stay shared. The `copy` module can copy a whole tree of objects. In Java this would take hand-written code or serialization.

```python
import copy

original = [[1], [2]]

# A shallow copy: a new outer list, the same inner ones.
shallow = original.copy()
shallow[0].append(9)
print(original)  # [[1, 9], [2]]

# A deep copy: everything is new.
deep = copy.deepcopy(original)
deep[0].append(7)
print(original)  # [[1, 9], [2]]
```

### Why numbers must not be compared with `is`

This is the same story as `Integer` in Java. There, values from -128 to 127 are cached, so `==` sometimes works and sometimes does not. CPython caches small numbers too, and the bound depends on the version. So `is` is used only with `None`, `True`, `False` and `enum` elements.

```java
Integer small1 = 127, small2 = 127;
System.out.println(small1 == small2);  // true

Integer big1 = 1000, big2 = 1000;
System.out.println(big1 == big2);  // false
System.out.println(big1.equals(big2));  // true
```

```python
small1 = int("256")
small2 = int("256")
print(small1 is small2)  # True

big1 = int("100000")
big2 = int("100000")
print(big1 is big2)  # False
print(big1 == big2)  # True
```

### Where a variable is visible

In Java a variable lives inside its block of curly braces. Python has no block scope: `if` and `for` do not create one. Only functions, classes and modules draw boundaries. A variable is looked up in four places in order, known as LEGB: Local (its own function), Enclosing (the outer function), Global (the file), Built-in (built-in names).

```java
for (int i = 0; i < 3; i++) {
    int square = i * i;
}

// Compile error: i and square do not exist here.
// System.out.println(i);
```

```python
for i in range(3):
    square = i * i

# Both variables are alive: a loop creates no scope of its own.
print(i, square)  # 2 4

# An exception: a variable inside a comprehension is not visible outside.
squares = [n * n for n in range(3)]
```

Reading an outer variable just works. But an assignment inside a function creates a local variable. To write to an outer one, you must say so with `global` or `nonlocal`.

```python
count = 0


def broken():

    # The assignment below makes count local in the whole function,
    # so the read fails: UnboundLocalError.
    count += 1


def works():
    global count
    count += 1


works()
print(count)  # 1


def counter():
    total = 0

    def add():

        # nonlocal is a variable of the outer function.
        nonlocal total
        total += 1
        return total

    return add


add = counter()
add()
print(add())  # 2
```

### What counts as true in a condition

Empty and zero values count as false, and everything else as true. A class of your own controls this through the `__bool__` or `__len__` methods.

```python
print(bool(0), bool(0.0), bool(""), bool(None))  # False False False False
print(bool([]), bool({}), bool(set()), bool(()))  # False False False False
print(bool(1), bool("0"), bool([0]))  # True True True

# bool is a subclass of int, so True equals 1. This is how matches are counted.
print(True + True)  # 2
print(sum(n > 1 for n in [1, 2, 3]))  # 2
```

### What "ask forgiveness, not permission" means

In Java you usually check first and act afterwards. In Python you more often act right away and handle the error. This style is called EAFP (easier to ask forgiveness than permission). It also removes the race between the check and the action. Exceptions are not considered a costly rarity here; they are routine.

```java
var config = new HashMap<>(Map.of("port", "80"));
int port = 0;

if (config.containsKey("port")) {
    port = Integer.parseInt(config.get("port"));
}
System.out.println(port);  // 80
```

```python
config = {"port": "80"}

try:
    port = int(config["port"])
except (KeyError, ValueError):
    port = 0
print(port)  # 80
```

### How your own class plugs into the language

In Java `for` works with `Iterable`, and `+` works only with strings and numbers. In Python every operator and built-in function has a corresponding dunder method. Declare it in your class, and the operator works. This set of rules is called the data model.

| What you write | What gets called | Java counterpart |
|---|---|---|
| `Cart()` | `__init__` | constructor |
| `len(x)` | `__len__` | `size()` |
| `x[i]` | `__getitem__` | `get(i)` |
| `x[i] = v` | `__setitem__` | `set(i, v)` |
| `v in x` | `__contains__` | `contains(v)` |
| `for v in x` | `__iter__` | `iterator()` |
| `a + b` | `__add__` | an `add` method |
| `a == b` | `__eq__` | `equals` |
| `a < b` | `__lt__` | `compareTo` |
| `hash(x)` | `__hash__` | `hashCode` |
| `str(x)`, `repr(x)` | `__str__`, `__repr__` | `toString` |
| `x()` | `__call__` | a functional interface method |
| `with x:` | `__enter__`, `__exit__` | `AutoCloseable` |

```java
class Cart implements Iterable<String> {
    private final List<String> items = new ArrayList<>();

    void add(String item) {
        items.add(item);
    }

    int size() {
        return items.size();
    }

    String get(int index) {
        return items.get(index);
    }

    public Iterator<String> iterator() {
        return items.iterator();
    }
}

var cart = new Cart();
cart.add("tea");
System.out.println(cart.size());  // 1
System.out.println(cart.get(0));  // tea
```

```python
class Cart:
    def __init__(self):
        self._items = []

    def add(self, item):
        self._items.append(item)

    def __len__(self):
        return len(self._items)

    def __getitem__(self, index):
        return self._items[index]

    def __iter__(self):
        return iter(self._items)

    def __contains__(self, item):
        return item in self._items


cart = Cart()
cart.add("tea")

# The object behaves like a built-in collection.
print(len(cart))  # 1
print(cart[0])  # tea
print("tea" in cart)  # True
print(list(cart))  # ['tea']

# An empty cart is false because its len is 0.
print(bool(Cart()))  # False
```

Operators are overloaded the same way. Comparisons are conveniently derived from one method: `__lt__` replaces `Comparable`, and the `total_ordering` decorator fills in `<=`, `>` and `>=`.

```java
record Money(int cents) implements Comparable<Money> {
    Money add(Money other) {
        return new Money(cents + other.cents);
    }

    public int compareTo(Money other) {
        return Integer.compare(cents, other.cents);
    }
}

var a = new Money(100);
var b = new Money(50);
System.out.println(a.add(b));  // Money[cents=150]
System.out.println(a.compareTo(b) > 0);  // true
```

```python
from dataclasses import dataclass
from functools import total_ordering


@total_ordering
@dataclass(frozen=True)
class Money:
    cents: int

    def __add__(self, other):
        return Money(self.cents + other.cents)

    def __lt__(self, other):
        return self.cents < other.cents


a = Money(100)
b = Money(50)
print(a + b)  # Money(cents=150)
print(a > b)  # True

# min, max and sorted now just work.
print(min(a, b))  # Money(cents=50)
```

## Functions: what Java does not have

### Why a list must not be a default value

A default value is computed once, when the function is declared, and is stored in it. If it is a list, all calls share the same object. This is Python's best-known trap.

```python
# 1. A list is created here, once.
def add_bad(item, items=[]):
    items.append(item)
    return items


# 2. The first call puts 1 in it.
print(add_bad(1))  # [1]

# 3. The second call gets the same list.
print(add_bad(2))  # [1, 2]


# Correct: None by default, and a new list inside.
def add_good(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items


print(add_good(1))  # [1]
print(add_good(2))  # [2]
```

### What a closure is

A closure is a function that remembers variables from the place where it was created. Java lambdas can do this too, but capture only `final` values. Python captures the variable itself, so it can be changed, and its latest value is what is seen.

```java
Function<Integer, Integer> makeAdder(int base) {
    return x -> x + base;
}

var addTen = makeAdder(10);
System.out.println(addTen.apply(5));  // 15

var tasks = new ArrayList<Supplier<Integer>>();
for (int i = 0; i < 3; i++) {

    // Does not compile without this copy: i changes.
    int copy = i;
    tasks.add(() -> copy);
}
System.out.println(tasks.get(0).get());  // 0
```

```python
def make_adder(base):
    return lambda x: x + base


add_ten = make_adder(10)
print(add_ten(5))  # 15

# A trap: all lambdas see the same variable i, and the loop is over.
tasks = [lambda: i for i in range(3)]
print(tasks[0]())  # 2

# The fix: a default value is computed at once and remembered.
tasks = [lambda i=i: i for i in range(3)]
print(tasks[0]())  # 0
```

### What a decorator is

The `@something` notation above a function looks like a Java annotation but works differently. An annotation is a mark that someone else reads later, such as Spring. A decorator is an ordinary function. It receives your function and returns another one that replaces it. This resembles the proxies Spring creates for `@Transactional`. Here is how it works, step by step.

```python
def logged(func):

    # 1. When add is declared, logged(add) is called.
    # 2. It creates wrapper and returns it in place of add.
    def wrapper(*args, **kwargs):
        print("before")  # 3. before
        result = func(*args, **kwargs)
        print("after")  # 5. after
        return result

    return wrapper


@logged
def add(a, b):
    print("add")  # 4. add
    return a + b


# The name add now points to wrapper.
print(add(2, 3))  # 6. 5
```

The `@logged` form is only a short way of writing an assignment. Here are the same two variants:

```python
@logged
def add(a, b):
    return a + b


# Exactly the same:
def add(a, b):
    return a + b


add = logged(add)
```

A real decorator has one more detail. `wrapper` replaces the function, so its name and documentation are lost. `functools.wraps` brings them back. A decorator can also take parameters; this needs one more level of nesting: `@retry(times=3)` first calls `retry(times=3)`, and the result is what acts as the decorator.

```python
from functools import wraps


# 1. retry(times=3) creates and returns decorator.
def retry(times):

    # 2. decorator(flaky) creates and returns wrapper.
    def decorator(func):

        # 3. Calling flaky() now runs wrapper.
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except ConnectionError:
                    if attempt == times - 1:
                        raise

        return wrapper

    return decorator


calls = []


@retry(times=3)
def flaky():
    calls.append(1)
    if len(calls) < 3:
        raise ConnectionError
    return "ok"


print(flaky())  # ok
print(len(calls))  # 3

# Without wraps this would be wrapper.
print(flaky.__name__)  # flaky
```

### Which ready-made decorators exist

Result caching, like `@Cacheable` in Spring, is in the standard library. Dispatch on the type of the first argument partly replaces overloading.

```python
from functools import lru_cache, singledispatch


# Stores call results by arguments: at most the last 128.
@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)


print(fib(50))  # 12586269025
print(fib.cache_info().hits)  # 48


# Chooses the implementation by the type of the first argument.
@singledispatch
def show(value):
    return "other"


@show.register
def _(value: int):
    return "number"


@show.register
def _(value: str):
    return "text"


print(show(1))  # number
print(show("a"))  # text
print(show(1.5))  # other
```

## Classes: what else you need to know

### How a class field differs from an object field

When reading `user.role`, Python looks for the attribute first on the object and then on the class. A write always creates the attribute on the object. Because of this, a class field holding a list ends up shared by all objects.

```python
class User:
    role = "guest"
    tags = []


a = User()
b = User()

# Reading the class field through the object.
print(a.role)  # guest

# The write created a field on a; the class and b are as before.
a.role = "admin"
print(b.role)  # guest

# A trap: this is not a write but a change to the shared list.
a.tags.append("x")
print(b.tags)  # ['x']

# An object's fields live in a dictionary, so they can be added on the fly.
a.city = "Kyiv"
print(a.__dict__)  # {'role': 'admin', 'city': 'Kyiv'}
```

A dictionary per object takes memory. If there are millions of objects, fields are declared in advance through `__slots__`. As a bonus, a stray field with a typo can no longer be added. A `dataclass` has the parameter `slots=True` for this.

```python
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x = x
        self.y = y


point = Point(1, 2)
try:
    point.z = 3
except AttributeError as e:
    print(type(e).__name__)  # AttributeError
```

### How multiple inheritance works

In Java class inheritance is single, and shared behaviour moves to default interface methods. In Python a class can have several parents. The order in which a method is looked up is called the MRO (Method Resolution Order). It is linear, and you can inspect it. An important point: `super()` calls not the parent but the next class in this order.

```python
class Base:
    def run(self):
        print("Base")  # 3. Base


class Left(Base):
    def run(self):
        print("Left")  # 1. Left
        super().run()


class Right(Base):
    def run(self):
        print("Right")  # 2. Right
        super().run()


class Both(Left, Right):
    pass


# The order: Both, Left, Right, Base, object. After Left comes Right, not Base.
names = [c.__name__ for c in Both.__mro__]
print(names)  # ['Both', 'Left', 'Right', 'Base', 'object']
Both().run()
```

In practice multiple inheritance is used for mixins. A mixin is a small class with a single capability that is mixed into the main one. It is the closest thing to default interface methods.

```java
interface Jsonable {
    Map<String, Object> fields();

    default String toJson() {
        return fields().toString();
    }
}

record User(String name) implements Jsonable {
    public Map<String, Object> fields() {
        return Map.of("name", name);
    }
}

System.out.println(new User("Ann").toJson());  // {name=Ann}
```

```python
import json


class JsonMixin:
    def to_json(self):
        return json.dumps(self.__dict__)


class User(JsonMixin):
    def __init__(self, name):
        self.name = name


print(User("Ann").to_json())  # {"name": "Ann"}
```

### How to access a field by name

Reflection in Python is simple and built into the language. To access an attribute by name there are the functions `getattr`, `setattr` and `hasattr`.

```java
class User {
    public String name = "Ann";

    public String greet() {
        return "Hi, " + name;
    }
}

var user = new User();
var field = User.class.getField("name");
System.out.println(field.get(user));  // Ann

var method = User.class.getMethod("greet");
System.out.println(method.invoke(user));  // Hi, Ann
```

```python
class User:
    name = "Ann"

    def greet(self):
        return f"Hi, {self.name}"


user = User()
print(getattr(user, "name"))  # Ann
print(getattr(user, "age", 0))  # 0
print(hasattr(user, "greet"))  # True

# A method is an ordinary attribute: get it and call it.
print(getattr(user, "greet")())  # Hi, Ann

# Methods can be replaced at run time. This is monkey patching.
User.greet = lambda self: "Hello"
print(user.greet())  # Hello
```

The `__getattr__` method is called when an attribute is not found the normal way. With it a proxy takes a couple of lines, where Java needs `java.lang.reflect.Proxy` and an interface.

```python
class Logged:
    def __init__(self, target):
        self._target = target

    # 1. Logged has no append attribute, so __getattr__ is called.
    # 2. It takes the attribute from the real object.
    def __getattr__(self, name):
        print(f"access {name}")  # access append
        return getattr(self._target, name)


items = Logged([])

# 3. The method obtained is called with the argument 1.
items.append(1)
```

### What else you will see in other people's code

These are rarely needed, but framework code has them.

| What | What it does | Java counterpart |
|---|---|---|
| `__new__` | creates an object, while `__init__` fills it | none |
| Descriptor | an object with `__get__` and `__set__` in a class field. This is how `property` and methods work | none |
| Metaclass | a class that creates classes. This is how Django builds models from their fields | annotation processing and bytecode generation |
| `__init_subclass__` | called when a subclass is declared, handy for registration | `ServiceLoader` |
| `__del__` | a finalizer you cannot rely on; use `with` instead | `finalize` |

## Type hints

### Does Python check types

No. Annotations are stored but have no effect at run time. A separate program checks them, most often mypy or pyright, run like a linter in CI. It amounts to the Java compiler, only optional. In services annotations are written everywhere, and checking runs in strict mode.

```python
def double(value: int) -> int:
    return value * 2


# Python runs this with no error. Only mypy reports it.
print(double("ab"))  # abab

# Annotations can be read. Pydantic and FastAPI are built on this.
print(double.__annotations__["value"].__name__)  # int
```

### How types are written

| Java | Python |
|---|---|
| `int`, `String` | `int`, `str` |
| `List<String>` | `list[str]` |
| `Map<String, Integer>` | `dict[str, int]` |
| `Set<Long>` | `set[int]` |
| `int[]` | `list[int]` |
| a pair of values | `tuple[str, int]` |
| an immutable list | `tuple[str, ...]` |
| `@Nullable String` | `str \| None` |
| `Object` | `object` |
| no checking at all | `Any` |
| `Collection<? extends T>` | `Iterable[T]`, `Sequence[T]` |
| `Function<Integer, String>` | `Callable[[int], str]` |
| `Class<T>` | `type[T]` |
| `void` | `None` |
| `sealed interface` with variants | `A \| B` |
| a limited set of strings | `Literal["asc", "desc"]` |
| returns this | `Self` |
| never returns | `NoReturn` |
| `typealias`: not in Java | `type UserId = int` |

In older code types look different. When you meet these, know that they mean the same thing.

| Old form | New form | Since version |
|---|---|---|
| `List[str]`, `Dict[str, int]` | `list[str]`, `dict[str, int]` | 3.9 |
| `Optional[str]` | `str \| None` | 3.10 |
| `Union[int, str]` | `int \| str` | 3.10 |
| `T = TypeVar("T")`, `Generic[T]` | `class Box[T]:` | 3.12 |

A `Sequence[T]` parameter accepts both a list and a tuple and cannot be changed. This is the same idea as `List<? extends T>`: to accept subtypes, a parameter must be read-only. So function parameters are better with broad types such as `Iterable` and `Sequence`, and results with a concrete `list`.

```python
from collections.abc import Callable, Iterable


def total(items: Iterable[int]) -> int:
    return sum(items)


# Any iterable collection fits.
print(total([1, 2]))  # 3
print(total({3, 4}))  # 7


def apply(func: Callable[[int], int], value: int) -> int:
    return func(value)


print(apply(lambda x: x + 1, 1))  # 2
```

## Iterators and generators

### How the `for` loop works

These are the same `Iterable` and `Iterator` as in Java. The `__iter__` method gives an iterator. The `__next__` method gives the next value, and the end is signalled not through `hasNext` but with a `StopIteration` exception.

```java
var iterator = List.of("a", "b").iterator();

while (iterator.hasNext()) {
    System.out.print(iterator.next());  // ab
}
```

```python
# 1. for calls iter(items).
iterator = iter(["a", "b"])

# 2. On each step it calls next(iterator).
print(next(iterator))  # a
print(next(iterator))  # b

# 3. StopIteration ends the for loop; nobody sees it.
print(next(iterator, "end"))  # end
```

### What a generator is

Writing an iterator in Java means a class with fields holding the state and two methods. Python has a generator for this: a function with `yield` instead of `return`. It gives out a value and freezes on that line, keeping all its variables, and on the next request continues from the same place. The numbers show the order of execution.

```python
def numbers():
    print("start")  # 2. start
    yield 1
    print("resume")  # 4. resume
    yield 2
    print("end")  # 6. end


# 1. The call only creates a generator; the body does not run yet.
gen = numbers()

# 2. next starts the body and stops at the first yield.
print(next(gen))  # 3. 1

# 4. The next next resumes from where it stopped.
print(next(gen))  # 5. 2

# 6. The body reached its end: that is StopIteration, so no more values.
print(next(gen, "no more"))  # 7. no more
```

Compare the same task: yielding the numbers from `start` to `end`.

```java
class Range implements Iterable<Integer> {
    final int from, to;

    Range(int from, int to) {
        this.from = from;
        this.to = to;
    }

    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            int current = from;

            public boolean hasNext() {
                return current < to;
            }

            public Integer next() {
                return current++;
            }
        };
    }
}

for (int n : new Range(1, 3)) {
    System.out.print(n);  // 12
}
```

```python
def count_range(start, end):
    current = start
    while current < end:
        yield current
        current += 1


for n in count_range(1, 3):
    print(n)  # 1 2
```

### What generators are for

They are lazy, like Java streams: a value appears only when requested. So you can describe an infinite sequence, and a large file can pass through a chain of functions without being loaded into memory.

```java
var firstFive = Stream.iterate(0, n -> n + 2)
    .limit(5)
    .toList();
System.out.println(firstFive);  // [0, 2, 4, 6, 8]
```

```python
from itertools import islice


# An endless loop is safe here: the code waits on yield until asked.
def evens():
    n = 0
    while True:
        yield n
        n += 2


# islice is limit.
print(list(islice(evens(), 5)))  # [0, 2, 4, 6, 8]
```

```java
var lines = Stream.of("1", "x", "2");

int total = lines
    .filter(line -> line.matches("\\d+"))
    .mapToInt(Integer::parseInt)
    .sum();
System.out.println(total);  // 3
```

```python
lines = ["1", "x", "2"]

# Parentheses: a generator expression. Each step is lazy, as in a stream.
digits = (line for line in lines if line.isdigit())
numbers = (int(line) for line in digits)

# The work starts only here.
print(sum(numbers))  # 3
```

The difference between square and round brackets is the main thing to remember here. The first form builds the whole list in memory at once; the second builds nothing.

```python
import sys

as_list = [n for n in range(1_000_000)]
as_gen = (n for n in range(1_000_000))

print(sys.getsizeof(as_list) > 1_000_000)  # True
print(sys.getsizeof(as_gen) < 1_000)  # True
```

A generator, like a stream, can be walked only once. A second pass raises no error; it is simply empty. The same goes for the results of `map`, `filter` and `zip`.

```java
var stream = Stream.of(1, 2);
stream.toList();

// Again: IllegalStateException.
// stream.toList();
```

```python
gen = (n for n in [1, 2])
print(list(gen))  # [1, 2]

# No error, just empty.
print(list(gen))  # []

doubled = map(lambda n: n * 2, [1, 2])
print(sum(doubled))  # 6
print(sum(doubled))  # 0
```

### What else generators can do

`yield from` passes on the values of another generator. The `send` method pushes a value inside, into the spot stopped at `yield`. Asynchrony used to be built on this mechanism; today you rarely meet it.

```python
def chain_all(first, second):

    # Replaces a loop with a yield for each value.
    yield from first
    yield from second


print(list(chain_all([1], [2])))  # [1, 2]


def averager():
    total = 0
    count = 0
    average = None
    while True:

        # First gives a value out, then receives a new one from send.
        value = yield average
        total += value
        count += 1
        average = total / count


avg = averager()

# The first next brings the generator to the first yield.
next(avg)
print(avg.send(10))  # 10.0
print(avg.send(20))  # 15.0
```

Ready-made lazy operations live in the `itertools` module.

```python languages="Java, Python"
from itertools import batched, chain, groupby, islice, takewhile  # Python

Stream.of(1, 2, 3).limit(2).toList()  # Java: [1, 2]
list(islice([1, 2, 3], 2))  # Python: [1, 2]

Stream.concat(Stream.of(1), Stream.of(2)).toList()  # Java: [1, 2]
list(chain([1], [2]))  # Python: [1, 2]

Stream.of(1, 2, 3).takeWhile(i -> i < 3).toList()  # Java: [1, 2]
list(takewhile(lambda i: i < 3, [1, 2, 3]))  # Python: [1, 2]

# Split into batches of 2.
list(batched([1, 2, 3], 2))  # Python: [(1, 2), (3,)]

# Groups only consecutive values, so sort first.
[k for k, _ in groupby(sorted("abab"))]  # Python: ['a', 'b']
```

## Threads and processes

### What the GIL is

The GIL (Global Interpreter Lock) is one shared lock for the whole process. Only the thread holding it can run Python code, so only one thread works at a time, however many cores there are. The threads themselves are real, system threads, as in Java. While waiting for the network, the disk or `sleep`, a thread releases the lock and others run.

The takeaway is simple: threads speed up waiting but not computation. Computation needs processes, each of which has its own GIL.

| Task | Java | Python |
|---|---|---|
| Lots of waiting on network or database | threads, virtual threads | `asyncio` or threads |
| Computation on all cores | threads | processes |
| A blocking library | threads | threads |

### How to start a thread

The `Thread` class is almost the same. Instead of implementing `Runnable`, you pass it a function and its arguments.

```java
void work(String name) {
    System.out.println("work " + name);  // work a
}

var thread = new Thread(() -> work("a"), "worker");
thread.setDaemon(true);
thread.start();
thread.join();

System.out.println(thread.getName());  // worker
System.out.println(thread.isAlive());  // false
```

```python
import threading


def work(name):
    print(f"work {name}")  # work a


# args is a tuple of arguments, so the comma is required.
thread = threading.Thread(target=work, args=("a",), name="worker", daemon=True)
thread.start()
thread.join()

print(thread.name)  # worker
print(thread.is_alive())  # False
```

Here is what "threads speed up waiting" means. Four threads sleep for 0.1 seconds each, and the total time is also about 0.1 seconds. Replace `sleep` with counting in a loop, and they run strictly in turn.

```python
import threading
import time


def wait():
    time.sleep(0.1)


threads = [threading.Thread(target=wait) for _ in range(4)]
start = time.perf_counter()

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(time.perf_counter() - start < 0.3)  # True
```

### Is synchronization needed when there is a GIL

Yes. The GIL protects the interpreter's own internals, not your code. The operation `counter += 1` has several steps, and a thread can lose the GIL between them. As in Java, a lock solves this. The form `with lock:` corresponds to `synchronized`: the lock is released on leaving the block, even after an error.

```text
Thread A                     Thread B
1. read counter = 5
                            2. read counter = 5
                            3. wrote counter = 6
4. wrote counter = 6
Two increments, yet the counter grew by 1.
```

```java
class Counter {
    private int value;

    synchronized void add() {
        value++;
    }

    synchronized int get() {
        return value;
    }
}

var counter = new Counter();
var threads = new ArrayList<Thread>();
for (int i = 0; i < 4; i++) {
    threads.add(Thread.ofPlatform().start(() -> {
        for (int j = 0; j < 10_000; j++) {
            counter.add();
        }
    }));
}
for (var thread : threads) {
    thread.join();
}
System.out.println(counter.get());  // 40000
```

```python
import threading


class Counter:
    def __init__(self):
        self.value = 0
        self._lock = threading.Lock()

    def add(self):
        with self._lock:
            self.value += 1


def run(counter):
    for _ in range(10_000):
        counter.add()


counter = Counter()
threads = [threading.Thread(target=run, args=(counter,)) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()
print(counter.value)  # 40000
```

Careful: `Lock` in Python is not reentrant, meaning the same thread cannot take it twice. `synchronized` and `ReentrantLock` in Java allow this. The exact counterpart is `RLock`.

```python
import threading

# A plain Lock: a second acquire in the same thread would hang forever.
lock = threading.Lock()
lock.acquire()
print(lock.acquire(blocking=False))  # False
lock.release()

# An RLock can be taken again.
rlock = threading.RLock()
with rlock:
    with rlock:
        print("inside")  # inside
```

### What replaces `ExecutorService`

A thread pool works the same way. `submit` returns a `Future`, and `result()` is `get()`. Instead of `try` with `shutdown`, `with` is used: on leaving the block the pool waits for all tasks and closes.

```java
int square(int n) {
    return n * n;
}

try (var pool = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = pool.submit(() -> square(3));
    System.out.println(future.get());  // 9

    var tasks = new ArrayList<Callable<Integer>>();
    for (int n : List.of(1, 2)) {
        tasks.add(() -> square(n));
    }
    for (var done : pool.invokeAll(tasks)) {
        System.out.print(done.get() + " ");  // 1 4
    }
}
```

```python
from concurrent.futures import ThreadPoolExecutor, as_completed


def square(n):
    return n * n


with ThreadPoolExecutor(max_workers=4) as pool:
    future = pool.submit(square, 3)
    print(future.result())  # 9

    # map: results in the order of the arguments.
    print(list(pool.map(square, [1, 2])))  # [1, 4]

    # as_completed: in order of completion, like CompletionService.
    futures = [pool.submit(square, n) for n in [1, 2]]
    print(sorted(f.result() for f in as_completed(futures)))  # [1, 4]
```

An exception from a task behaves as in Java: it is stored in the `Future` and raised when `result()` is called. If nobody asks for the result, the error is silently lost.

### What corresponds to what in `java.util.concurrent`

| Java | Python |
|---|---|
| `Thread` | `threading.Thread` |
| `synchronized`, `ReentrantLock` | `threading.RLock` |
| `Semaphore` | `threading.Semaphore` |
| `CountDownLatch` | `threading.Event`, `Barrier` |
| `CyclicBarrier` | `threading.Barrier` |
| `Condition` | `threading.Condition` |
| `BlockingQueue` | `queue.Queue` |
| `ConcurrentHashMap` | `dict` plus `Lock` |
| `AtomicInteger` | `int` plus `Lock` |
| `ThreadLocal` | `threading.local` |
| `ExecutorService` | `ThreadPoolExecutor` |
| `ForkJoinPool` | `ProcessPoolExecutor` |
| `Future` | `concurrent.futures.Future` |
| `CompletableFuture` | `asyncio.Task` |
| `volatile` | not needed |

### How to make a producer and a consumer

`queue.Queue` is a `BlockingQueue`: `put` waits if the queue is full, and `get` waits if it is empty. The numbers show the order of work. To finish, a special value goes into the queue; in Java it is called a poison pill.

```java
var tasks = new ArrayBlockingQueue<Integer>(10);
var results = new ArrayList<Integer>();

var consumer = Thread.ofPlatform().start(() -> {
    try {
        int item;
        while ((item = tasks.take()) != -1) {
            results.add(item * 2);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
});

tasks.put(1);
tasks.put(2);
tasks.put(-1);
consumer.join();
System.out.println(results);  // [2, 4]
```

```python
import queue
import threading

tasks = queue.Queue(maxsize=10)
results = []


def consume():

    # 2. Take from the queue; if empty, the thread waits.
    while (item := tasks.get()) is not None:
        results.append(item * 2)


consumer = threading.Thread(target=consume)
consumer.start()

# 1. Put the tasks.
tasks.put(1)
tasks.put(2)

# 3. None says there is no more work.
tasks.put(None)
consumer.join()
print(results)  # [2, 4]
```

### How to stop a thread

Python has no `interrupt` method, and a thread cannot be stopped from outside. The thread itself checks a flag, and `Event` is there for that. Its `wait` method also replaces `sleep`, waking up as soon as the flag is set.

```java
var worker = Thread.ofPlatform().start(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            return;
        }
    }
});

worker.interrupt();
worker.join();
System.out.println(worker.isAlive());  // false
```

```python
import threading

stop = threading.Event()


def worker():

    # Wait 10 seconds or for the stop signal, whichever comes first.
    while not stop.wait(timeout=10):
        pass


thread = threading.Thread(target=worker)
thread.start()

stop.set()
thread.join()
print(thread.is_alive())  # False
```

### How to compute on all cores

`ProcessPoolExecutor` looks like a thread pool but starts separate processes. Each has its own interpreter, memory and GIL, so computation runs in parallel. The price: arguments and results are copied between processes through serialization (called pickle), so a lambda cannot be passed and there are no shared variables. The startup code must live inside `if __name__ == "__main__"`, because a new process imports the file again.

```java
var result = IntStream.of(1, 2, 3, 4)
    .parallel()
    .map(n -> n * n)
    .sum();
System.out.println(result);  // 30
```

```python
from concurrent.futures import ProcessPoolExecutor


def square(n):
    return n * n


if __name__ == "__main__":
    with ProcessPoolExecutor() as pool:
        print(sum(pool.map(square, [1, 2, 3, 4])))  # 30
```

The GIL is gradually going away. Since 3.13 there is a separate interpreter build without it; since 3.14 that build is officially supported but not the default, and some libraries are not ready for it yet. In it, threads run in parallel, as in Java. Since 3.14 there is also `InterpreterPoolExecutor`, where each thread has its own isolated interpreter and GIL.

## Asynchrony: asyncio

### What asyncio is

It is a way to serve thousands of waiting operations on one thread. A thread in Python is costly and runs into the GIL, so services are usually built on asyncio. In Java Netty and WebFlux solve the same task, and now virtual threads too. You will need four terms:

- **A coroutine** is a function declared with `async def`. It can pause and then continue working.
- **`await`** is a pause point. The coroutine says: "I am waiting, deal with the others for now".
- **The event loop** is a dispatcher, one per thread. It picks which coroutine works next.
- **A task** is a coroutine scheduled on the event loop. It is the counterpart of `CompletableFuture`.

Compare with virtual threads. The code does the same thing: two waiting operations run at the same time.

```java
String fetch(String name) throws InterruptedException {
    Thread.sleep(100);
    return name + " done";
}

try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    var user = pool.submit(() -> fetch("user"));
    var orders = pool.submit(() -> fetch("orders"));
    System.out.println(user.get());  // user done
    System.out.println(orders.get());  // orders done
}
```

```python
import asyncio


async def fetch(name):
    await asyncio.sleep(0.1)
    return f"{name} done"


async def main():

    # gather starts both coroutines and waits for both.
    user, orders = await asyncio.gather(fetch("user"), fetch("orders"))
    print(user)  # user done
    print(orders)  # orders done


# asyncio.run creates an event loop and runs main in it.
asyncio.run(main())
```

### How it works, step by step

There is one thread, and all the work takes turns. Switching happens only at `await`. The numbers show the actual order of output.

```python
import asyncio


async def first():
    print("first starts")  # 1. first starts

    # first gives control to the loop, and it starts second.
    await asyncio.sleep(0.2)
    print("first ends")  # 4. first ends


async def second():
    print("second starts")  # 2. second starts

    # Now both wait, and the thread is free.
    await asyncio.sleep(0.1)
    print("second ends")  # 3. second ends


async def main():
    await asyncio.gather(first(), second())


asyncio.run(main())
```

### Why calling an `async def` does nothing

Calling `fetch()` does not run the code; it only creates a coroutine object. The code runs through `await` or `create_task`. The second trap is that `await` alone creates no concurrency: two `await`s in a row run one after the other. For operations to run at once, you need tasks.

```java
CompletableFuture<String> fetch(String name) {
    return CompletableFuture.supplyAsync(() -> name);
}

var user = fetch("user");
var orders = fetch("orders");
System.out.println(user.join() + " " + orders.join());  // user orders
```

```python
import asyncio
import time


async def fetch(name):
    await asyncio.sleep(0.1)
    return name


async def one_by_one():

    # The second starts only after the first: 0.2 seconds.
    start = time.perf_counter()
    await fetch("user")
    await fetch("orders")
    print(round(time.perf_counter() - start, 1))  # 0.2


async def at_once():
    start = time.perf_counter()

    # create_task queues the coroutine at once, like supplyAsync.
    user = asyncio.create_task(fetch("user"))
    orders = asyncio.create_task(fetch("orders"))

    # await is join: wait for the result.
    print(await user, await orders)  # user orders
    print(round(time.perf_counter() - start, 1))  # 0.1


asyncio.run(one_by_one())
asyncio.run(at_once())
```

### What happens if you call a blocking function

This is the main mistake in async code. A plain `time.sleep`, a database driver or `requests` has no `await`, so it does not give up control. The event loop stands still, and all other requests wait. In WebFlux the same happens with a blocking call on a Netty thread. Such calls must go to a separate thread through `asyncio.to_thread`.

```python
import asyncio
import time


async def bad():

    # The whole event loop stands still: other tasks do not run for 0.1 seconds.
    time.sleep(0.1)


async def good():

    # The function goes to another thread, and the loop keeps working.
    await asyncio.to_thread(time.sleep, 0.1)


async def measure(work):
    start = time.perf_counter()
    await asyncio.gather(work(), work())
    return round(time.perf_counter() - start, 1)


print(asyncio.run(measure(bad)))  # 0.2
print(asyncio.run(measure(good)))  # 0.1
```

### How to run a group of tasks properly

`TaskGroup` arrived in 3.11 and is the counterpart of `StructuredTaskScope` in Java. The rule is simple: tasks do not outlive the `async with` block. If one fails, the others are cancelled, and the errors arrive together in an exception group. A special form, `except*`, handles them. `gather` has none of this: on an error the other tasks keep running on their own.

```python
import asyncio


async def ok():
    await asyncio.sleep(0.2)
    print("never printed")


async def fail():
    await asyncio.sleep(0.1)
    raise ValueError("boom")


async def main():
    try:
        async with asyncio.TaskGroup() as tasks:

            # 1. Start two tasks.
            tasks.create_task(ok())
            tasks.create_task(fail())

    # 2. fail failed, so ok is cancelled.
    # 3. except* catches the given type from the error group.
    except* ValueError as group:
        print(group.exceptions)  # (ValueError('boom'),)


asyncio.run(main())
```

### How to set a timeout and cancel a task

Cancellation works like this: `task.cancel()` raises `CancelledError` inside the coroutine at its `await`. This resembles `InterruptedException`. It must not be caught and swallowed, or the task cannot stop, so cleanup goes in `finally`. `CancelledError` does not inherit from `Exception`, so a plain `except Exception` does not catch it.

```java
var slow = CompletableFuture.supplyAsync(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    return "done";
});

try {
    slow.orTimeout(50, TimeUnit.MILLISECONDS).join();
} catch (CompletionException e) {
    System.out.println(e.getCause() instanceof TimeoutException);  // true
}
```

```python
import asyncio


async def slow():
    try:
        await asyncio.sleep(1)
    finally:
        print("cleanup")  # 2. cleanup


async def main():
    try:

        # 1. After 0.05 seconds the task is cancelled.
        async with asyncio.timeout(0.05):
            await slow()

    # 3. Outside, the cancellation becomes TimeoutError.
    except TimeoutError:
        print("timed out")  # 3. timed out


asyncio.run(main())
```

### Why a task can disappear

The event loop keeps only weak references to tasks. If you start a task and do not keep what `create_task` returned, the garbage collector can delete it mid-run. Also, an error in such a task is seen by nobody. So keep the reference, or start tasks in a `TaskGroup`.

```python
import asyncio

background = set()


def fire_and_forget(coroutine):
    task = asyncio.create_task(coroutine)

    # Keep a reference while the task runs, then remove it.
    background.add(task)
    task.add_done_callback(background.discard)


async def notify():
    print("sent")  # sent


async def main():
    fire_and_forget(notify())
    await asyncio.sleep(0)


asyncio.run(main())
```

### How to limit the number of concurrent tasks

Creating a hundred thousand tasks is easy, but a database cannot take that many requests. As with virtual threads in Java, the limit comes from a semaphore.

```java
var limit = new Semaphore(10);

try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100; i++) {
        pool.submit(() -> {
            limit.acquire();
            try {
                return 1;
            } finally {
                limit.release();
            }
        });
    }
}
```

```python
import asyncio


async def fetch(limit, n):

    # No more than 10 tasks inside the block at once.
    async with limit:
        await asyncio.sleep(0)
        return n


async def main():
    limit = asyncio.Semaphore(10)
    results = await asyncio.gather(*(fetch(limit, n) for n in range(100)))
    print(len(results))  # 100


asyncio.run(main())
```

### Are locks needed in asyncio

Between two `await`s code runs without interruption, since there is one thread. So `counter += 1` is safe here. A race appears only if there is an `await` between the read and the write. `asyncio.Lock` exists for that case, but a plain `threading.Lock` must not be used here: it would stop the whole event loop.

```python
import asyncio

balance = 100


async def withdraw(lock, amount):
    global balance
    async with lock:

        # 1. Read the balance.
        current = balance

        # 2. Without the lock a second task gets here with the same balance.
        await asyncio.sleep(0)

        # 3. Wrote the new balance.
        balance = current - amount


async def main():
    lock = asyncio.Lock()
    await asyncio.gather(withdraw(lock, 10), withdraw(lock, 10))
    print(balance)  # 80


asyncio.run(main())
```

### What `async with` and `async for` are

These are the same `with` and `for`, except that their inner steps contain `await` themselves: opening a connection, say, or getting the next batch from the network. An async generator is written with `yield`, like a normal one, only inside `async def`. It resembles a `Flux` in WebFlux.

```python
import asyncio


class Connection:
    async def __aenter__(self):
        await asyncio.sleep(0)
        print("open")  # 1. open
        return self

    async def __aexit__(self, exc_type, exc, tb):
        await asyncio.sleep(0)
        print("close")  # 3. close


async def rows():
    for row in [1, 2]:
        await asyncio.sleep(0)
        yield row


async def main():
    async with Connection():
        async for row in rows():
            print(row)  # 2. 1 2


asyncio.run(main())
```

### What replaces `ThreadLocal`

A plain `threading.local` will not do here: there is one thread, and all tasks would see one value. `ContextVar` exists for this. Each task gets its own copy of the context. This is the counterpart of `ScopedValue` in Java and MDC in logging: a request id need not be passed through every call.

```java
var requestId = new ThreadLocal<String>();
requestId.set("a");
System.out.println(requestId.get());
```

```python
import asyncio
from contextvars import ContextVar

request_id = ContextVar("request_id", default="-")


def log(message):
    print(request_id.get(), message)  # a work b work


async def handle(rid):
    request_id.set(rid)
    await asyncio.sleep(0)
    log("work")


async def main():
    await asyncio.gather(handle("a"), handle("b"))

    # In main itself the value did not change.
    print(request_id.get())  # -


asyncio.run(main())
```

### How to connect threads and asyncio

| From | To | How |
|---|---|---|
| Regular code | a coroutine | `asyncio.run(main())` |
| A coroutine | a blocking function | `await asyncio.to_thread(func)` |
| Another thread | a coroutine | `asyncio.run_coroutine_threadsafe(coro, loop)` |
| Another thread | a plain function on the loop | `loop.call_soon_threadsafe(func)` |

### How asyncio differs from virtual threads

The goal is the same: cheap waiting. The difference is that virtual threads hide this inside the JVM, and ordinary blocking code keeps working. In asyncio the asynchrony shows in the code: an `async` function can be called only from another such function. This is called "function colouring".

| | Virtual threads | asyncio |
|---|---|---|
| CPU cores | all | one |
| Where they switch | at any blocking call | only at `await` |
| Blocking code | works | stops everything |
| Races in code | as with ordinary threads | only across `await` |
| Cancellation | `interrupt` | `CancelledError` |
| Libraries | ordinary | must be async |

## Dependencies and tests

### What replaces Maven and Gradle

In Java dependencies sit in a shared `~/.m2` cache, and each project gets its versions through the classpath. In Python an installed library is visible to the whole interpreter copy, so each project has its own copy in a `.venv` directory. Such a directory is called a virtual environment. The project description lives in `pyproject.toml` and the exact versions in a lock file. Today the uv tool usually manages all of this.

| Java | Python |
|---|---|
| `pom.xml`, `build.gradle` | `pyproject.toml` |
| `gradle.lockfile` | `uv.lock` |
| Maven Central | PyPI |
| `mvn dependency:tree` | `uv tree` |
| `gradle dependencies add` | `uv add httpx` |
| `gradle build` | `uv build` |
| `gradle test` | `uv run pytest` |
| Checkstyle, Spotless | `ruff` |
| `javac` as a type check | `mypy` |
| `.jar` | `.whl` |

An import in Python looks for modules along the `sys.path` directory list, like the classpath in Java. Unlike in Java, a circular import is dangerous here: importing runs the file, so if `a.py` imports `b.py` and that imports `a.py` again, the second sees a half-run module and fails with `ImportError`. The cure is moving shared code to a third module.

### What replaces JUnit

The de facto standard is pytest. A test is a plain function whose name starts with `test_`, and a plain `assert` replaces the `assertEquals` family. On failure, pytest itself shows what was compared with what. Instead of `@BeforeEach` there is a fixture: a function whose name the test gives as a parameter. Everything after `yield` in a fixture runs after the test, like `@AfterEach`.

```java
class CartTest {
    private List<String> cart;

    @BeforeEach
    void setUp() {
        cart = new ArrayList<>();
    }

    @Test
    void addsItem() {
        cart.add("tea");
        assertEquals(1, cart.size());
    }

    @ParameterizedTest
    @CsvSource({"2, 4", "3, 9"})
    void squares(int input, int expected) {
        assertEquals(expected, input * input);
    }

    @Test
    void rejectsBadNumber() {
        assertThrows(NumberFormatException.class, () -> Integer.parseInt("x"));
    }
}
```

```python
import pytest


# Replaces @BeforeEach: pytest calls it before each test.
@pytest.fixture
def cart():
    return []


# The parameter name matches the fixture name.
def test_adds_item(cart):
    cart.append("tea")
    assert len(cart) == 1


@pytest.mark.parametrize("value, expected", [(2, 4), (3, 9)])
def test_squares(value, expected):
    assert value * value == expected


def test_rejects_bad_number():
    with pytest.raises(ValueError):
        int("x")
```

### What replaces Mockito

Mocks are in the standard library, in the `unittest.mock` module. A `Mock` accepts any call and remembers it. The `patch` function replaces a name in a given module for the duration of a test. An important rule: replace the name where it is used, not where it is declared.

```java
var repository = mock(UserRepository.class);
when(repository.find(1)).thenReturn("Ann");

assertEquals("Ann", repository.find(1));
verify(repository).find(1);
```

```python
from unittest.mock import Mock

repository = Mock()
repository.find.return_value = "Ann"

print(repository.find(1))  # Ann
repository.find.assert_called_once_with(1)

# Make a method raise an exception: thenThrow.
repository.save.side_effect = ConnectionError("down")
```

Although patching works, code is easier to test as in Java: through dependencies passed to the constructor. The service at the end of the article is written this way.

## Memory and performance

### How Python manages memory

In the JVM objects live until the garbage collector finds them. In CPython every object has a reference count, and an object is deleted as soon as the count reaches zero. Circular references cannot be detected this way, so there is an additional cycle collector in the `gc` module.

| | JVM | CPython |
|---|---|---|
| When an object is deleted | when the collector gets there | at once, when no references remain |
| Moving objects | yes, the heap compacts | no |
| Size settings | `-Xmx` | none; the container limits it |
| Size of `int` | 4 bytes | about 28 bytes |

### What to do if it is slow

Measure first. `cProfile` is a simple profiler, and for a running service there is py-spy, which attaches from outside. What usually helps fastest is the right data structure and built-in functions written in C: `sum`, `sorted`, `set`. Heavy computation is done in C libraries such as NumPy, which release the GIL while working.

## Service architecture in Python

### What replaces Tomcat and the Servlet API

The scheme is the same: a server accepts connections, and a standard interface lets it call your application. There are two interfaces: WSGI for synchronous code and ASGI for asynchronous.

| Java | Python | What it is |
|---|---|---|
| Servlet API | WSGI | a synchronous server-application interface |
| WebFlux `HttpHandler` | ASGI | an asynchronous interface |
| Tomcat, Jetty | gunicorn | a WSGI server |
| Netty | uvicorn | an ASGI server |
| Spring MVC | Django, Flask | synchronous frameworks |
| WebFlux | FastAPI | an asynchronous framework |

### How the working model differs from Java

A Java service is one process with a thread pool and shared memory. Because of the GIL, a Python service is several processes, called workers. It follows that:

- **Scaling.** In Kubernetes you usually keep one worker per pod and add replicas.
- **An in-memory cache** is separate per worker. Shared data goes in Redis or the database.
- **A connection pool** is also separate per worker: 4 workers with 10 connections each make 40 database connections.
- **A scheduler** would start in every worker, so it moves to a separate process.
- **Long computation** in a request handler is unacceptable. It goes to a task queue such as Celery.

### How to do without Spring

A dependency container is almost never needed. Dependencies come through the constructor, and instead of the Spring context there is one function at startup that creates objects and passes them to each other. This place is called the composition root. The layers are the same, only they run through modules rather than packages of classes.

| Spring | Python module | What it knows |
|---|---|---|
| `@Entity` | `domain.py` | only the standard library |
| repository interfaces | `ports.py` | the domain |
| `@Service` | `service.py` | the domain and ports |
| `@Repository` | `storage.py` | the domain and the database |
| `@RestController` | `http_api.py` | the service and HTTP |
| `@Configuration`, `main` | `__main__.py` | everything |

### Which Java patterns are not needed here

Many Java patterns work around language limits that Python lacks. For example, a strategy in Java is an interface and a class, while in Python it is just a function.

```java
interface Discount {
    double apply(double price);
}

class Seasonal implements Discount {
    public double apply(double price) {
        return price * 0.9;
    }
}

double checkout(double price, Discount discount) {
    return discount.apply(price);
}

System.out.println(checkout(100, new Seasonal()));  // 90.0
```

```python
def seasonal(price):
    return price * 0.9


def checkout(price, discount):
    return discount(price)


print(checkout(100, seasonal))  # 90.0
```

| Java | Python |
|---|---|
| Singleton | an object at module level |
| Factory | a function or the class itself |
| Builder | named arguments and `replace` |
| Strategy, Command | pass a function |
| Observer | a list of functions |
| Visitor | `match`, `singledispatch` |
| Decorator | `@decorator` |
| Iterator | a generator |
| Proxy | `__getattr__` |
| Utility class | functions in a module |
| Getters and setters | a field, later `@property` |

Code written with Java habits is instantly visible. Here is what gives it away:

```python
# Bad
for i in range(len(items)):
    print(items[i])

# Good
for item in items:
    print(item)

# Bad
if len(items) == 0:
    pass

# Good
if not items:
    pass

# Bad
if value == None or flag == True:
    pass

# Good
if value is None or flag:
    pass

# Bad
user.get_name()

# Good
user.name
```

## A microservice on the standard library

### What we are building

Let us put it all together and write an orders service. It has no third-party libraries, so that the language itself is visible. In a real project you would hand the HTTP server and routes to FastAPI, and PostgreSQL would replace SQLite, but only the files at the edges change. The service cancels unpaid orders older than an hour in the background.

| Request | What it does | Response |
|---|---|---|
| `GET /health` | checks that the service is alive | 200 |
| `POST /orders` | create an order | 201, 422 |
| `GET /orders?status=paid` | list orders | 200 |
| `GET /orders/{id}` | show an order | 200, 404 |
| `POST /orders/{id}/pay` | pay | 200, 409 |
| `POST /orders/{id}/cancel` | cancel | 200, 409 |

A request goes through the files from top to bottom, and each file knows only about its lower neighbour. The logic in the centre does not depend on the outside world. It talks to it through interfaces, called **ports**, and their implementations for a given technology are called **adapters**. In Spring this is a service working with a repository interface, unaware that JDBC is behind it.

```text
http_server.py    reads bytes, builds a Request object
      │
      ▼
http_api.py       picks a route, parses JSON
      │
      ▼
service.py        creates, pays, cancels an order
      │        │
      ▼        ▼
domain.py    storage.py
rules      SQLite queries
```

```text
orders-service/
├── pyproject.toml
├── Dockerfile
├── src/orders/
│   ├── __init__.py       an empty file marking a package
│   ├── __main__.py       startup and wiring of objects
│   ├── config.py         settings from the environment
│   ├── domain.py         the order and its rules
│   ├── ports.py          what the service needs from outside
│   ├── service.py        business logic
│   ├── storage.py        the SQLite adapter
│   ├── http_server.py    an HTTP server on asyncio
│   ├── http_api.py       routes and errors
│   └── logs.py           JSON logs
└── tests/
    ├── test_service.py
    └── test_api.py
```

### How to describe the project

The `pyproject.toml` file is the counterpart of `pom.xml`.

```toml title="pyproject.toml"
[project]
name = "orders-service"
version = "0.1.0"
requires-python = ">=3.12"

# No third-party libraries are needed to run.
dependencies = []

# Creates an orders-service command that calls main.
[project.scripts]
orders-service = "orders.__main__:main"

# Like testImplementation in Gradle: they stay out of the image.
[dependency-groups]
dev = ["mypy>=1.13", "pytest>=8", "ruff>=0.8"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/orders"]

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.mypy]
strict = true
files = ["src", "tests"]

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "ASYNC", "G"]
```

### How to describe an order

Let us start from the centre. This file replaces an `Order` class with business methods. It knows nothing of HTTP or the database.

```python title="src/orders/domain.py"
from dataclasses import dataclass, replace
from datetime import datetime
from decimal import Decimal
from enum import StrEnum
from typing import Self


class OrderStatus(StrEnum):
    NEW = "new"
    PAID = "paid"
    CANCELLED = "cancelled"


# A common parent: the web layer catches all errors with one except.
class DomainError(Exception):
    pass


class OrderNotFound(DomainError):
    pass


class InvalidOrder(DomainError):
    pass


class IllegalTransition(DomainError):
    pass


# An immutable record: pay and cancel return a new order.
@dataclass(frozen=True, slots=True)
class Order:
    id: str
    customer: str
    amount: Decimal
    status: OrderStatus
    created_at: datetime

    # A check on creation. NaN and Infinity are valid Decimals too.
    def __post_init__(self) -> None:
        if not self.customer.strip():
            raise InvalidOrder("customer must not be empty")
        if not self.amount.is_finite() or self.amount <= 0:
            raise InvalidOrder("amount must be a positive number")

    def pay(self) -> Self:
        if self.status is not OrderStatus.NEW:
            raise IllegalTransition(f"cannot pay an order that is {self.status}")
        return replace(self, status=OrderStatus.PAID)

    def cancel(self) -> Self:
        if self.status is not OrderStatus.NEW:
            raise IllegalTransition(f"cannot cancel an order that is {self.status}")
        return replace(self, status=OrderStatus.CANCELLED)
```

### How to separate logic from the database

This is the counterpart of an `OrderRepository` interface. An implementation does not write `implements`, so tests can use a simple fake that inherits from nothing.

```python title="src/orders/ports.py"
from collections.abc import Sequence
from typing import Protocol

from .domain import Order, OrderStatus


# The methods are async: while the database answers, the loop serves others.
class OrderRepository(Protocol):
    async def add(self, order: Order) -> None: ...

    async def get(self, order_id: str) -> Order | None: ...

    async def find(
        self, status: OrderStatus | None, limit: int
    ) -> Sequence[Order]: ...

    # Saves the new status only if the database still holds expected.
    async def update_status(self, order: Order, expected: OrderStatus) -> bool: ...
```

### How to write the business logic

This is the counterpart of a class with `@Service`. In Java the clock would be a `Clock`, and identifiers an interface with an implementation. Here passing a function is enough, and a test easily supplies another one.

```python title="src/orders/service.py"
from collections.abc import Callable, Sequence
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from uuid import uuid4

from .domain import IllegalTransition, Order, OrderNotFound, OrderStatus
from .ports import OrderRepository


def _utc_now() -> datetime:
    return datetime.now(UTC)


def _new_id() -> str:
    return uuid4().hex


class OrderService:

    # Parameters after * are passed by name only.
    def __init__(
        self,
        repository: OrderRepository,
        *,
        clock: Callable[[], datetime] = _utc_now,
        new_id: Callable[[], str] = _new_id,
    ) -> None:
        self._repository = repository
        self._clock = clock
        self._new_id = new_id

    async def create(self, customer: str, amount: Decimal) -> Order:
        order = Order(
            self._new_id(), customer, amount, OrderStatus.NEW, self._clock()
        )
        await self._repository.add(order)
        return order

    async def get(self, order_id: str) -> Order:
        order = await self._repository.get(order_id)
        if order is None:
            raise OrderNotFound(f"no order {order_id}")
        return order

    async def find(
        self, status: OrderStatus | None, limit: int = 100
    ) -> Sequence[Order]:
        return await self._repository.find(status, limit)

    # Order.pay is an ordinary function whose first parameter is self.
    async def pay(self, order_id: str) -> Order:
        return await self._change(order_id, Order.pay)

    async def cancel(self, order_id: str) -> Order:
        return await self._change(order_id, Order.cancel)

    async def expire_unpaid(self, older_than: timedelta) -> int:
        cutoff = self._clock() - older_than
        expired = 0
        for order in await self._repository.find(OrderStatus.NEW, limit=1000):
            if order.created_at < cutoff:

                # bool is an int, so True adds 1.
                expired += await self._repository.update_status(
                    order.cancel(), OrderStatus.NEW
                )
        return expired

    async def _change(
        self, order_id: str, transition: Callable[[Order], Order]
    ) -> Order:

        # 1. Read the order.
        order = await self.get(order_id)

        # 2. Checked the rules and got a new order.
        changed = transition(order)

        # 3. A conditional write: another request may have changed it during the await.
        if not await self._repository.update_status(changed, expected=order.status):
            raise IllegalTransition(f"order {order_id} was changed concurrently")
        return changed
```

Step 3 of `_change` is called optimistic locking: the record is not locked in advance, and on saving, it is checked for changes. In JPA `@Version` solves the same task. Here is what would happen without it on two simultaneous payments:

```text
Request A                         Request B
1. reads the order: new
                                 2. reads the order: new
3. UPDATE ... WHERE status='new'
   1 row updated, success
                                 4. UPDATE ... WHERE status='new'
                                    0 rows updated, error 409
```

### How to call a database from async code

This is the counterpart of a class with `@Repository`. The `sqlite3` driver blocks, so each call goes to a separate thread. For PostgreSQL you would take an async driver, and `to_thread` would vanish while the port stays unchanged.

```python title="src/orders/storage.py"
import asyncio
import sqlite3
from collections.abc import Callable, Sequence
from contextlib import closing
from datetime import datetime
from decimal import Decimal
from pathlib import Path

from .domain import Order, OrderStatus

_SCHEMA = """
    CREATE TABLE IF NOT EXISTS orders (
        id         TEXT PRIMARY KEY,
        customer   TEXT NOT NULL,
        amount     TEXT NOT NULL,
        status     TEXT NOT NULL,
        created_at TEXT NOT NULL
    );
    CREATE INDEX IF NOT EXISTS orders_status ON orders (status);
"""


def _to_order(row: sqlite3.Row) -> Order:
    return Order(
        id=row["id"],
        customer=row["customer"],
        amount=Decimal(row["amount"]),
        status=OrderStatus(row["status"]),
        created_at=datetime.fromisoformat(row["created_at"]),
    )


class SqliteOrderRepository:
    def __init__(self, path: Path) -> None:
        self._path = path

    async def migrate(self) -> None:
        await self._run(lambda c: c.executescript(_SCHEMA))

    async def add(self, order: Order) -> None:
        values = (
            order.id,
            order.customer,
            str(order.amount),
            order.status,
            order.created_at.isoformat(),
        )
        query = "INSERT INTO orders VALUES (?, ?, ?, ?, ?)"
        await self._run(lambda c: c.execute(query, values))

    async def get(self, order_id: str) -> Order | None:
        query = "SELECT * FROM orders WHERE id = ?"
        row = await self._run(lambda c: c.execute(query, (order_id,)).fetchone())
        return _to_order(row) if row else None

    async def find(self, status: OrderStatus | None, limit: int) -> Sequence[Order]:
        where, args = ("WHERE status = ?", [status]) if status else ("", [])
        query = f"SELECT * FROM orders {where} ORDER BY created_at LIMIT ?"
        rows = await self._run(lambda c: c.execute(query, (*args, limit)).fetchall())
        return [_to_order(row) for row in rows]

    async def update_status(self, order: Order, expected: OrderStatus) -> bool:
        query = "UPDATE orders SET status = ? WHERE id = ? AND status = ?"
        values = (order.status, order.id, expected)
        return await self._run(lambda c: c.execute(query, values).rowcount) == 1

    # Like TransactionTemplate: open, run, commit or roll back.
    async def _run[T](self, work: Callable[[sqlite3.Connection], T]) -> T:
        def in_thread() -> T:
            connection = sqlite3.connect(self._path, timeout=5.0)
            connection.row_factory = sqlite3.Row

            # with connection commits or rolls back, and closing closes it.
            with closing(connection), connection:
                return work(connection)

        return await asyncio.to_thread(in_thread)
```

### How to add a request id to logs

This is the role of Logback and MDC. Each request is handled in its own task, so it has its own `ContextVar` value. The code writing to the log knows nothing about the id.

```python title="src/orders/logs.py"
import json
import logging
from contextvars import ContextVar

request_id: ContextVar[str] = ContextVar("request_id", default="-")


# One log record is one JSON line, easy for log collectors to parse.
class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        entry = {
            "time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
            "level": record.levelname,
            "logger": record.name,
            "request_id": request_id.get(),
            "message": record.getMessage(),
        }
        if record.exc_info:
            entry["exception"] = self.formatException(record.exc_info)
        return json.dumps(entry, ensure_ascii=False)


def configure_logging(level: str) -> None:
    handler = logging.StreamHandler()
    handler.setFormatter(JsonFormatter())
    logging.basicConfig(level=level, handlers=[handler], force=True)
```

### How to write an HTTP server on asyncio

In a real project uvicorn does this work. It is written here to show `asyncio` on a real task. An HTTP request is plain text: a request line, headers, an empty line and a body.

```http languages="HTTP, JSON"
POST /orders HTTP/1.1
Host: localhost:8080
Content-Length: 38

{"customer": "ann", "amount": "19.99"}
```

```python title="src/orders/http_server.py"
import asyncio
import json
import logging
from collections.abc import Awaitable, Callable
from contextlib import suppress
from dataclasses import dataclass
from http import HTTPStatus
from typing import Any
from urllib.parse import parse_qsl, urlsplit

log = logging.getLogger(__name__)

MAX_BODY = 1024 * 1024


@dataclass(frozen=True, slots=True)
class Request:
    method: str
    path: str
    query: dict[str, str]
    headers: dict[str, str]
    body: bytes


@dataclass(frozen=True, slots=True)
class Response:
    status: HTTPStatus
    payload: Any = None

    def encode(self) -> bytes:
        body = (
            b"" if self.payload is None else json.dumps(self.payload).encode("utf-8")
        )
        head = (
            f"HTTP/1.1 {self.status.value} {self.status.phrase}\r\n"
            "Content-Type: application/json\r\n"
            f"Content-Length: {len(body)}\r\n"
            "Connection: close\r\n\r\n"
        )

        # The protocol requires latin-1 for headers.
        return head.encode("latin-1") + body


# A type alias: any async function from Request to Response.
type Handler = Callable[[Request], Awaitable[Response]]


async def read_request(reader: asyncio.StreamReader) -> Request:

    # 1. Read bytes up to the empty line: these are the headers.
    head = await reader.readuntil(b"\r\n\r\n")
    request_line, *header_lines = head.decode("latin-1").split("\r\n")
    method, target, _version = request_line.split(" ")

    # 2. Parse the headers into a dictionary.
    headers = {}
    for line in header_lines:
        name, separator, value = line.partition(":")
        if separator:
            headers[name.strip().lower()] = value.strip()

    # 3. Learn the body length from Content-Length and read it.
    length = int(headers.get("content-length", "0"))
    if length > MAX_BODY:
        raise ValueError("body is too large")
    body = await reader.readexactly(length)

    url = urlsplit(target)
    return Request(method, url.path, dict(parse_qsl(url.query)), headers, body)


class HttpServer:
    def __init__(
        self,
        handler: Handler,
        host: str,
        port: int,
        *,
        max_concurrent: int = 100,
        read_timeout: float = 5.0,
        handler_timeout: float = 30.0,
    ) -> None:
        self._handler = handler
        self._host = host
        self._port = port
        self._slots = asyncio.Semaphore(max_concurrent)
        self._read_timeout = read_timeout
        self._handler_timeout = handler_timeout

        # The loop keeps only weak references to tasks, so we hold our own.
        self._connections: set[asyncio.Task[Any]] = set()
        self._server: asyncio.Server | None = None

    # For each connection asyncio creates a task running _on_connect.
    async def start(self) -> int:
        self._server = await asyncio.start_server(
            self._on_connect, self._host, self._port
        )
        return int(self._server.sockets[0].getsockname()[1])

    async def stop(self, grace: float) -> None:
        assert self._server is not None

        # 1. Stop accepting new connections.
        self._server.close()

        # 2. Give the current ones time to finish, and cancel the rest.
        if self._connections:
            _, unfinished = await asyncio.wait(self._connections, timeout=grace)
            for task in unfinished:
                task.cancel()
            await asyncio.gather(*unfinished, return_exceptions=True)
        await self._server.wait_closed()

    async def _on_connect(
        self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
    ) -> None:
        task = asyncio.current_task()
        assert task is not None
        self._connections.add(task)
        task.add_done_callback(self._connections.discard)
        try:
            response = await self._respond(reader)
            if response is not None:
                writer.write(response.encode())

                # If the client reads slowly, the task waits here.
                await writer.drain()
        except ConnectionError:
            log.info("client disconnected")
        finally:
            writer.close()
            with suppress(ConnectionError):
                await writer.wait_closed()

    async def _respond(self, reader: asyncio.StreamReader) -> Response | None:

        # The timeout guards against a client that connects and sends nothing.
        try:
            async with asyncio.timeout(self._read_timeout):
                request = await read_request(reader)
        except asyncio.IncompleteReadError:
            return None
        except (ValueError, asyncio.LimitOverrunError):
            return Response(HTTPStatus.BAD_REQUEST, {"error": "malformed request"})
        except TimeoutError:
            return Response(
                HTTPStatus.REQUEST_TIMEOUT, {"error": "request not read in time"}
            )

        # The semaphore guards against overload, the timeout against a stuck call.
        async with self._slots:
            try:
                async with asyncio.timeout(self._handler_timeout):
                    return await self._handler(request)
            except TimeoutError:
                log.warning("timed out: %s %s", request.method, request.path)
                return Response(HTTPStatus.GATEWAY_TIMEOUT, {"error": "timed out"})

            # CancelledError does not land here: cancellation must pass through.
            except Exception:
                log.exception("failed: %s %s", request.method, request.path)
                return Response(
                    HTTPStatus.INTERNAL_SERVER_ERROR, {"error": "internal error"}
                )
```

### How to make routes and error handling

This is the role of `@RestController` and `@ControllerAdvice`. Routes are conveniently chosen with `match`: the pattern `"GET", ["orders", order_id]` fits `GET /orders/42` and immediately puts `42` in the `order_id` variable.

```python title="src/orders/http_api.py"
import json
import logging
from decimal import Decimal, InvalidOperation
from http import HTTPStatus
from uuid import uuid4

from .domain import (
    DomainError,
    IllegalTransition,
    InvalidOrder,
    Order,
    OrderNotFound,
    OrderStatus,
)
from .http_server import Request, Response
from .logs import request_id
from .service import OrderService

log = logging.getLogger(__name__)

_ERROR_STATUS: dict[type[DomainError], HTTPStatus] = {
    OrderNotFound: HTTPStatus.NOT_FOUND,
    InvalidOrder: HTTPStatus.UNPROCESSABLE_ENTITY,
    IllegalTransition: HTTPStatus.CONFLICT,
}


def _to_json(order: Order) -> dict[str, str]:
    return {
        "id": order.id,
        "customer": order.customer,
        "amount": str(order.amount),
        "status": order.status,
        "created_at": order.created_at.isoformat(),
    }


def _parse_new_order(body: bytes) -> tuple[str, Decimal]:
    try:
        payload = json.loads(body)
    except ValueError as e:
        raise InvalidOrder("body is not valid JSON") from e

    # The pattern checks both fields and types.
    # The amount is a string: a JSON number loses precision.
    match payload:
        case {"customer": str(customer), "amount": str(amount)}:
            try:
                return customer, Decimal(amount)
            except InvalidOperation as e:
                raise InvalidOrder(f"bad amount: {amount!r}") from e
        case _:
            raise InvalidOrder("expected strings: customer, amount")


class OrdersApi:
    def __init__(self, service: OrderService) -> None:
        self._service = service

    # The object can be called as a function, so it fits Handler.
    async def __call__(self, request: Request) -> Response:
        request_id.set(request.headers.get("x-request-id") or uuid4().hex)
        try:
            response = await self._route(request)

        # Like @ControllerAdvice: a business error becomes a response code.
        except DomainError as e:
            response = Response(_ERROR_STATUS[type(e)], {"error": str(e)})
        log.info("%s %s -> %d", request.method, request.path, response.status)
        return response

    async def _route(self, request: Request) -> Response:
        match request.method, request.path.strip("/").split("/"):
            case "GET", ["health"]:
                return Response(HTTPStatus.OK, {"status": "ok"})
            case "POST", ["orders"]:
                customer, amount = _parse_new_order(request.body)
                order = await self._service.create(customer, amount)
                return Response(HTTPStatus.CREATED, _to_json(order))
            case "GET", ["orders"]:
                return await self._list(request)
            case "GET", ["orders", order_id]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.get(order_id))
                )
            case "POST", ["orders", order_id, "pay"]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.pay(order_id))
                )
            case "POST", ["orders", order_id, "cancel"]:
                return Response(
                    HTTPStatus.OK, _to_json(await self._service.cancel(order_id))
                )
            case _:
                return Response(HTTPStatus.NOT_FOUND, {"error": "no such route"})

    async def _list(self, request: Request) -> Response:
        raw = request.query.get("status")
        try:
            status = OrderStatus(raw) if raw else None
        except ValueError as e:
            raise InvalidOrder(f"unknown status: {raw!r}") from e
        orders = await self._service.find(status)
        return Response(HTTPStatus.OK, [_to_json(order) for order in orders])
```

### How to read settings

This is the role of `@ConfigurationProperties`: an immutable object read once from environment variables. A test can pass a plain dictionary instead of the environment.

```python title="src/orders/config.py"
import os
from collections.abc import Mapping
from dataclasses import dataclass
from datetime import timedelta
from pathlib import Path


@dataclass(frozen=True)
class Settings:
    host: str = "0.0.0.0"
    port: int = 8080
    database: Path = Path("orders.db")
    log_level: str = "INFO"
    unpaid_ttl: timedelta = timedelta(hours=1)
    shutdown_grace: float = 10.0

    @classmethod
    def from_env(cls, env: Mapping[str, str] = os.environ) -> "Settings":
        return cls(
            host=env.get("ORDERS_HOST", cls.host),
            port=int(env.get("ORDERS_PORT", cls.port)),
            database=Path(env.get("ORDERS_DATABASE", cls.database)),
            log_level=env.get("ORDERS_LOG_LEVEL", cls.log_level),
        )
```

### Where all the objects are put together

This file is the whole dependency container. You can read it from top to bottom and see what connects to what. The numbers show how the service shuts down: Docker sends the `SIGTERM` signal, and the service finishes the current requests.

```python title="src/orders/__main__.py"
import asyncio
import logging
import signal
from contextlib import suppress
from datetime import timedelta

from .config import Settings
from .http_api import OrdersApi
from .http_server import HttpServer
from .logs import configure_logging
from .service import OrderService
from .storage import SqliteOrderRepository

log = logging.getLogger("orders")


# A background job, like @Scheduled(fixedDelay = ...).
async def expire_loop(
    service: OrderService, stop: asyncio.Event, ttl: timedelta, every: float = 60.0
) -> None:
    while not stop.is_set():
        try:
            if expired := await service.expire_unpaid(ttl):
                log.info("expired %d unpaid orders", expired)

        # One failure must not stop the job forever.
        except Exception:
            log.exception("expiration job failed")

        # Wait a minute or for the stop signal.
        with suppress(TimeoutError):
            async with asyncio.timeout(every):
                await stop.wait()


async def run(settings: Settings) -> None:
    repository = SqliteOrderRepository(settings.database)
    await repository.migrate()
    service = OrderService(repository)
    server = HttpServer(OrdersApi(service), settings.host, settings.port)

    # 1. Docker sends SIGTERM, and the handler sets the stop event.
    stop = asyncio.Event()
    loop = asyncio.get_running_loop()
    for sig in (signal.SIGINT, signal.SIGTERM):
        signal.signal(sig, lambda *_: loop.call_soon_threadsafe(stop.set))

    port = await server.start()
    log.info("listening on %s:%d", settings.host, port)

    # TaskGroup guarantees the background job does not outlive run.
    async with asyncio.TaskGroup() as tasks:
        tasks.create_task(expire_loop(service, stop, settings.unpaid_ttl))

        # 2. Here run wakes up.
        await stop.wait()
        log.info("shutting down")

        # 3. The server finishes the current requests and closes.
        await server.stop(settings.shutdown_grace)
    log.info("stopped")


def main() -> None:
    settings = Settings.from_env()
    configure_logging(settings.log_level)
    asyncio.run(run(settings))


if __name__ == "__main__":
    main()
```

### How to test the service

Unit tests work with a fake instead of a database. The `InMemoryOrders` class keeps orders in a dictionary. It nowhere inherits from `OrderRepository`, yet mypy checks that it fits.

```python title="tests/test_service.py"
import asyncio
from collections.abc import Sequence
from datetime import UTC, datetime, timedelta
from decimal import Decimal

import pytest

from orders.domain import (
    IllegalTransition,
    InvalidOrder,
    Order,
    OrderNotFound,
    OrderStatus,
)
from orders.service import OrderService

NOW = datetime(2026, 11, 5, 12, 0, tzinfo=UTC)


class InMemoryOrders:
    def __init__(self) -> None:
        self.orders: dict[str, Order] = {}

    async def add(self, order: Order) -> None:
        self.orders[order.id] = order

    async def get(self, order_id: str) -> Order | None:
        return self.orders.get(order_id)

    async def find(self, status: OrderStatus | None, limit: int) -> Sequence[Order]:
        found = [
            o for o in self.orders.values() if status is None or o.status is status
        ]
        return found[:limit]

    async def update_status(self, order: Order, expected: OrderStatus) -> bool:
        if self.orders[order.id].status is not expected:
            return False
        self.orders[order.id] = order
        return True


# The clock is frozen, so tests do not depend on the current time.
@pytest.fixture
def service() -> OrderService:
    return OrderService(InMemoryOrders(), clock=lambda: NOW, new_id=lambda: "id-1")


def test_new_order_can_be_paid(service: OrderService) -> None:
    async def scenario() -> None:
        order = await service.create("ann", Decimal("10.50"))
        assert (await service.pay(order.id)).status is OrderStatus.PAID
        assert (await service.get(order.id)).status is OrderStatus.PAID

    asyncio.run(scenario())


def test_paid_order_cannot_be_cancelled(service: OrderService) -> None:
    async def scenario() -> None:
        order = await service.create("ann", Decimal("1"))
        await service.pay(order.id)
        with pytest.raises(IllegalTransition):
            await service.cancel(order.id)

    asyncio.run(scenario())


@pytest.mark.parametrize(
    "customer, amount",
    [("", "1"), ("ann", "0"), ("ann", "-1"), ("ann", "NaN"), ("ann", "Infinity")],
)
def test_invalid_orders_are_rejected(
    service: OrderService, customer: str, amount: str
) -> None:
    with pytest.raises(InvalidOrder):
        asyncio.run(service.create(customer, Decimal(amount)))


def test_missing_order(service: OrderService) -> None:
    with pytest.raises(OrderNotFound):
        asyncio.run(service.get("nope"))


def test_old_unpaid_orders_expire() -> None:
    now = [NOW]
    service = OrderService(
        InMemoryOrders(), clock=lambda: now[0], new_id=lambda: "id-1"
    )

    async def scenario() -> None:
        order = await service.create("ann", Decimal("1"))
        assert await service.expire_unpaid(timedelta(hours=1)) == 0
        now[0] += timedelta(hours=2)
        assert await service.expire_unpaid(timedelta(hours=1)) == 1
        assert (await service.get(order.id)).status is OrderStatus.CANCELLED

    asyncio.run(scenario())
```

The integration test starts a real server on a free port (port 0 means "pick one yourself") with a real database in a temporary directory.

```python title="tests/test_api.py"
import asyncio
import json
from pathlib import Path
from typing import Any

from orders.http_api import OrdersApi
from orders.http_server import HttpServer
from orders.service import OrderService
from orders.storage import SqliteOrderRepository


async def call(
    port: int, method: str, path: str, body: Any = None
) -> tuple[int, Any]:
    payload = b"" if body is None else json.dumps(body).encode()
    head = (
        f"{method} {path} HTTP/1.1\r\n"
        f"Host: test\r\nContent-Length: {len(payload)}\r\n\r\n"
    )
    reader, writer = await asyncio.open_connection("127.0.0.1", port)
    writer.write(head.encode() + payload)
    await writer.drain()
    response = await reader.read()
    writer.close()
    await writer.wait_closed()
    status_line, _, content = response.partition(b"\r\n\r\n")
    return int(status_line.split()[1]), json.loads(content) if content else None


def test_order_lifecycle_over_http(tmp_path: Path) -> None:
    async def scenario() -> None:
        repository = SqliteOrderRepository(tmp_path / "test.db")
        await repository.migrate()
        server = HttpServer(OrdersApi(OrderService(repository)), "127.0.0.1", 0)
        port = await server.start()
        try:
            assert (await call(port, "GET", "/health"))[0] == 200

            order = {"customer": "ann", "amount": "19.99"}
            status, created = await call(port, "POST", "/orders", order)
            assert status == 201
            assert created["amount"] == "19.99"
            assert created["status"] == "new"
            pay = f"/orders/{created['id']}/pay"

            status, paid = await call(port, "POST", pay)
            assert (status, paid["status"]) == (200, "paid")
            assert (await call(port, "POST", pay))[0] == 409

            _, listed = await call(port, "GET", "/orders?status=paid")
            assert [o["id"] for o in listed] == [created["id"]]
            assert (await call(port, "GET", "/orders?status=lost"))[0] == 422

            assert (await call(port, "GET", "/orders/nope"))[0] == 404
            assert (await call(port, "POST", "/orders", {"amount": 5}))[0] == 422
            assert (await call(port, "DELETE", "/orders"))[0] == 404
        finally:
            await server.stop(grace=1.0)

    asyncio.run(scenario())


def test_malformed_request_gets_400(tmp_path: Path) -> None:
    async def scenario() -> None:
        repository = SqliteOrderRepository(tmp_path / "test.db")
        await repository.migrate()
        server = HttpServer(OrdersApi(OrderService(repository)), "127.0.0.1", 0)
        port = await server.start()
        try:
            reader, writer = await asyncio.open_connection("127.0.0.1", port)
            writer.write(b"nonsense\r\n\r\n")
            await writer.drain()
            assert b"400 Bad Request" in await reader.read()
            writer.close()
            await writer.wait_closed()
        finally:
            await server.stop(grace=1.0)

    asyncio.run(scenario())
```

### How to package the service in Docker

The build has two stages, as with Gradle and a JRE. Each step is explained in a comment.

```dockerfile title="Dockerfile"
# Stage 1: uv installs only runtime dependencies from the lock file.
FROM python:3.15-slim AS build
RUN pip install --no-cache-dir uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
COPY src ./src
RUN uv sync --frozen --no-dev --no-editable

# Stage 2: the finished environment is copied to a clean image.
FROM python:3.15-slim
RUN useradd --create-home app && mkdir /data && chown app /data
COPY --from=build /app/.venv /app/.venv
USER app
ENV ORDERS_DATABASE=/data/orders.db PATH=/app/.venv/bin:$PATH
EXPOSE 8080
VOLUME /data
HEALTHCHECK CMD python -c "import urllib.request as r; \
    r.urlopen('http://127.0.0.1:8080/health', timeout=2)"

# The array form: the service becomes PID 1, and SIGTERM goes straight to it.
CMD ["orders-service"]
```

### How to run the service

```shellscript
# Create the environment and uv.lock
uv sync

# Tests
uv run pytest

# Types in strict mode
uv run mypy

# The linter
uv run ruff check

# Run the service
uv run python -m orders

# Or in a container
docker build -t orders-service .
docker run --rm -p 8080:8080 orders-service
```

Let us create an order, pay for it, try to pay again and send bad data.

```shellscript languages="Shell, JSON"
$ curl -s -X POST localhost:8080/orders \
    -d '{"customer": "ann", "amount": "19.99"}' | python -m json.tool
{
    "id": "2165d2a6a1ee4cbb966203d6099d49c9",
    "customer": "ann",
    "amount": "19.99",
    "status": "new",
    "created_at": "2026-11-05T18:44:05.717023+00:00"
}

$ ID=2165d2a6a1ee4cbb966203d6099d49c9
$ curl -s -X POST localhost:8080/orders/$ID/pay | python -m json.tool
{
    "id": "2165d2a6a1ee4cbb966203d6099d49c9",
    "customer": "ann",
    "amount": "19.99",
    "status": "paid",
    "created_at": "2026-11-05T18:44:05.717023+00:00"
}

$ curl -s -X POST localhost:8080/orders/$ID/pay
{"error": "cannot pay an order that is paid"}

$ curl -s -X POST localhost:8080/orders \
    -d '{"customer": "a", "amount": "NaN"}'
{"error": "amount must be a positive number"}
```

### What frameworks will replace

This code is handwritten only to reveal the mechanisms. In a real project libraries take over most of it. The `service.py` written here does not change.

| Written here | What a project would use | Java counterpart |
|---|---|---|
| `http_server.py` | uvicorn, gunicorn | Tomcat, Netty |
| `http_api.py` | FastAPI, Flask, Django | Spring MVC |
| JSON parsing | Pydantic | Jackson, Bean Validation |
| `storage.py` | SQLAlchemy, asyncpg | Hibernate, JDBC |
| `CREATE TABLE` | Alembic | Flyway, Liquibase |
| `Settings` | pydantic-settings | `@ConfigurationProperties` |
| `expire_loop` | Celery, APScheduler | `@Scheduled`, Quartz |
| `retry` | tenacity | Resilience4j |
| `logs.py` | structlog | Logback, MDC |
| none | prometheus-client, OpenTelemetry | Micrometer |
| none | httpx | `WebClient` |
| none | aiokafka | Spring Kafka |

## What to read next

Primary sources: [the data model](https://docs.python.org/3/reference/datamodel.html), [the asyncio documentation](https://docs.python.org/3/library/asyncio.html), [the free-threading guide](https://docs.python.org/3/howto/free-threading-python.html), [the mypy documentation](https://mypy.readthedocs.io/) and [what is new in each version](https://docs.python.org/3/whatsnew/index.html). For comparison with Java this site has articles on [Java basics](/en/java-core-questions/), [multithreading](/en/java-kotlin-multithreading/), [Project Loom](/en/project-loom-manual/) and [Spring](/en/java-spring-concepts/).
