article thumbnail
Modern Python
Python quietly got a lot nicer. Have you?
5 min read
#programming, #python, #friday1

Your code still works. It runs, it passes tests, it ships. But if you learned Python a decade ago and never looked back, you're writing it the hard way — manually formatting strings, reaching for os.path, nesting if/elif ladders, and writing boilerplate classes by hand. Python quietly got a lot nicer. Here are the features you're leaving on the table, and the modern versions of habits you probably still have.

1. f-strings: Stop Concatenating

If you're still building strings with +, %, or .format(), this is the single biggest upgrade you can make. f-strings (Python 3.6+) are faster, shorter, and readable.

name = "Ada"
age = 36

# The old way
msg = "Hello, " + name + ". You are " + str(age) + "."
msg = "Hello, %s. You are %d." % (name, age)
msg = "Hello, {}. You are {}.".format(name, age)

# The modern way
msg = f"Hello, {name}. You are {age}."

They do math, call methods, and format inline:

price = 19.5
print(f"Total: ${price * 1.08:.2f}")        # Total: $21.06
print(f"{name.upper()} is {age} years old")  # ADA is 36 years old

# The self-documenting debug trick (3.8+) — note the trailing '='
print(f"{price=}")                            # price=19.5

2. pathlib: Stop Gluing Paths Together

String-based paths break across operating systems and read terribly. pathlib (3.4+) treats paths as objects.

import os
# The old way
config = os.path.join(os.path.dirname(__file__), "config", "settings.ini")
if os.path.exists(config):
    with open(config) as f:
        data = f.read()

# The modern way
from pathlib import Path
config = Path(__file__).parent / "config" / "settings.ini"
if config.exists():
    data = config.read_text()

Common wins:

Path("report.csv").suffix          # '.csv'
Path("report.csv").stem            # 'report'
list(Path(".").glob("*.py"))       # every .py file here
Path("out").mkdir(exist_ok=True)   # no more try/except on makedirs

3. dataclasses: Stop Writing Boilerplate Classes

If your class is mostly self.x = x in __init__, a dataclass (3.7+) writes it for you — including __init__, __repr__, and __eq__.

# The old way
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"
    def __eq__(self, other):
        return (self.x, self.y) == (other.x, other.y)

# The modern way
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

That's the whole thing. You get Point(1, 2), a readable repr, and == comparison for free. Add defaults, immutability, and more with almost no extra code:

@dataclass(frozen=True)   # immutable + hashable
class Config:
    host: str = "localhost"
    port: int = 8080

4. The match Statement: Beyond the if/elif Ladder

Structural pattern matching (3.10+) replaces long if/elif chains and can destructure data as it matches.

# The old way
if command["type"] == "move":
    x, y = command["x"], command["y"]
    move(x, y)
elif command["type"] == "quit":
    quit()
else:
    unknown()

# The modern way
match command:
    case {"type": "move", "x": x, "y": y}:
        move(x, y)
    case {"type": "quit"}:
        quit()
    case _:
        unknown()

It matches on shape, types, and values — not just equality.

5. The Walrus Operator: Assign and Test at Once

:= (3.8+) lets you assign a value and use it in the same expression. Great for loops and comprehensions that would otherwise compute something twice.

# The old way
data = get_data()
while data:
    process(data)
    data = get_data()

# The modern way
while (data := get_data()):
    process(data)

# Filter and reuse a computed value in one line
results = [y for x in items if (y := expensive(x)) > 0]

6. Type Hints: Let Your Editor Catch Bugs

Type hints (3.5+, much nicer in 3.10+) don't slow your code down — they're read by your editor and tools like mypy to catch mistakes before you run anything.

# The old way — what does this even take or return?
def get_user(id):
    ...

# The modern way — obvious, and your IDE autocompletes it
def get_user(id: int) -> dict | None:
    ...

# Modern union syntax (3.10+): use `X | Y`, not typing.Optional / typing.Union
def parse(value: str | int) -> float:
    ...

You don't have to type everything. Even hinting function signatures pays for itself in autocomplete and fewer "NoneType has no attribute" surprises.

7. enumerate and zip: Stop Managing Indexes by Hand

These aren't new, but they're chronically underused.

# The old way
i = 0
for item in items:
    print(i, item)
    i += 1

# The modern way
for i, item in enumerate(items):
    print(i, item)

# Walk two lists together instead of indexing both
for name, score in zip(names, scores):
    print(f"{name}: {score}")

Quick Reference: Old Habit → Modern Replacement

If you still write... Use instead Since
"x = " + str(x) f"x = {x}" 3.6
os.path.join(...) Path(a) / b 3.4
Hand-written __init__ @dataclass 3.7
Long if/elif chains match/case 3.10
data = f(); while data: while (data := f()): 3.8
Optional[str] str \| None 3.10
Manual index counter enumerate() 2.3
dict1.update(dict2) copy dict1 \| dict2 3.9

A Word on Versions

Most of these landed between Python 3.6 and 3.10, so unless you're stuck on a legacy interpreter, you already have them. The current stable release is Python 3.14, and every feature here is fully supported in it. Check what you're running with:

python --version

If you're on 3.10 or newer, every feature in this article is available to you today — no installs, no dependencies.

Modernization Checklist

✅ Search your codebase for .format( and % string formatting → convert to f-strings
✅ Replace os.path calls with pathlib.Path
✅ Turn data-holding classes into @dataclass
✅ Collapse deep if/elif ladders into match/case
✅ Add type hints to public function signatures
✅ Swap Optional[X]/Union[X, Y] for X | None / X | Y
✅ Replace manual index counters with enumerate()

Conclusion: None of this changes what your code does — it changes how much of it you have to write and read. Modern Python is shorter, safer, and clearer, and the features are already sitting in the interpreter you have installed. Pick one habit from the checklist, apply it to your next file, and you'll never go back to writing it like it's 2015.