Introduction
Python has shipped a steady stream of language features over the past few releases that quietly change how you write code — if you actually use them. Most developers don’t. The syntax exists, the docs explain it, and the everyday Python being written looks like Python from 2018.
Three features stand out as worth knowing in 2026. They’re not exotic — they’re in the standard interpreter, well-supported by type checkers and linters, and immediately useful. Match statements, dataclasses, and positional-or-keyword-only parameters. Here’s what each one does, when to reach for it, and the common mistakes when you start using them.
📚 Table of contents
- 1. The
matchstatement (structural pattern matching) - 2.
@dataclass— the boilerplate killer - 3. Positional-only and keyword-only parameters
- How they compose together
- Common mistakes
- FAQs
1. The match statement (structural pattern matching)
Added in Python 3.10. Looks like a switch; does much more. The simple form replaces
nested if/elif chains:
def describe(status):
match status:
case "success":
return "operation completed"
case "error":
return "an error occurred"
case "pending":
return "still in progress"
case _:
return "unknown status"
Same as a switch. The interesting part is structural matching — you can pattern-match against shapes, not just values:
def process(data):
match data:
case 0:
return "zero"
case 1 | 2 | 3: # OR of values
return f"small: {data}"
case [first, second]: # exactly 2-element list
return f"pair: {first}, {second}"
case [first, *rest]: # list with at least 1
return f"first={first} rest={rest}"
case {"name": name, "age": age}: # dict with these keys
return f"{name} is {age}"
case str(): # any string
return f"string: {data}"
case _:
return "unknown"
Patterns can be values, OR-combinations with |, list shapes, dict shapes, type
checks, and combinations of all the above. Captured variables (first,
second, name, age) bind during the match and are usable
inside the case body. Cases are tried in order — put more specific patterns first.
Where it pays off: parsing structured data, walking ASTs, dispatching on JSON shapes, handling
different message types in an event system. Anywhere you’d previously write
if isinstance(x, dict) and "name" in x and "age" in x: — match replaces it
with one readable line.
2. @dataclass — the boilerplate killer
Old-style data classes:
class User:
def __init__(self, id, name, roles=None):
self.id = id
self.name = name
self.roles = roles if roles is not None else []
def __repr__(self):
return f"User(id={self.id!r}, name={self.name!r}, roles={self.roles!r})"
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return (self.id, self.name, self.roles) == (other.id, other.name, other.roles)
Same thing with @dataclass:
from dataclasses import dataclass, field
@dataclass(frozen=True)
class User:
id: int
name: str
roles: list[str] = field(default_factory=list)
Six lines instead of fourteen, with stronger guarantees. The decorator generates
__init__, __repr__, __eq__, and (because of
frozen=True) __hash__ for you. Attempting to mutate
user.name = "x" on a frozen dataclass raises an exception.
Important gotcha: never write roles: list[str] = []. The mutable default sticks to
the class and silently shares state across instances — the same trap as mutable default
arguments. Use field(default_factory=list) instead.
Useful flags on the decorator:
frozen=True— immutable instances, hashable.order=True— generate__lt__,__le__, etc.slots=True— use__slots__for lower memory.kw_only=True(3.10+) — force keyword-only construction.
Dataclasses pair extremely well with the match statement — you can pattern-match on dataclass shapes directly:
@dataclass
class Product:
id: int; name: str; price: float; in_stock: bool
def categorize(p):
match p:
case Product(price=0):
return "free"
case Product(price=p) if p > 1000:
return "expensive"
case Product(in_stock=False):
return "unavailable"
case _:
return "regular"
3. Positional-only and keyword-only parameters
Two underused features of Python function signatures that pay off most when you’re writing
library code. Use / to mark parameters as positional-only, * to mark
them as keyword-only:
def greet(name, /, greeting="Hello", *, punctuation="!"):
return f"{greeting}, {name}{punctuation}"
greet("Alice") # OK
greet("Bob", "Hi") # OK — greeting positionally
greet("Charlie", greeting="Hey") # OK
greet("Dana", punctuation=".") # OK
greet(name="Eve") # TypeError — name is positional-only
greet("Frank", "Hello", "!") # TypeError — punctuation is keyword-only
Three rules:
- Everything before
/must be passed positionally. - Everything between
/and*can be passed either way. - Everything after
*must be passed as a keyword argument.
Why bother?
- API stability — positional-only means you can rename the internal parameter later without breaking callers. Keyword-only means callers can’t accidentally swap two arguments.
- Readability —
open("f", "r")reads fine positionally;complex_function(x=1, y=2, z=3)reads better with keyword arguments forced. - Consistency with built-ins — many Python built-ins already enforce positional-only (try
pow(base=2, exp=3)). - Documentation — the signature itself tells the caller how to call it. No guesswork.
Common pattern in production: positional-only for arguments without meaningful names (x, y,
index); keyword-only for boolean flags (force=True, strict=False) where
a bare positional True/False would be cryptic.
How they compose together
These three features layer cleanly:
from dataclasses import dataclass
@dataclass(frozen=True, slots=True, kw_only=True)
class Event:
type: str
payload: dict
def handle(event: Event, /, *, verbose: bool = False) -> str:
match event:
case Event(type="login", payload={"user": user}):
return f"login: {user}"
case Event(type="purchase", payload={"amount": amount, **rest}):
return f"purchase: ${amount}" + (f" extras={rest}" if verbose else "")
case Event(type=t):
return f"unhandled: {t}"
Frozen dataclass for the event, structural pattern match on its shape, positional-only first parameter so you can rename it later, keyword-only verbose flag for readability. Idiomatic modern Python in eight lines.
❌ Common mistakes
- Mutable defaults on a dataclass field. Use
field(default_factory=list), not= []. - Ordering match cases wrong. The most specific pattern must come first; the catch-all (
case _) must be last. - Using match for simple value dispatch where a dict lookup would be clearer.
- Adding positional-only enforcement to internal functions where it gains nothing and just confuses readers.
- Forgetting that frozen dataclasses are hashable. If you mutate something inside (e.g. a list field), the hash silently goes wrong.
- Skipping
slots=Trueon dataclasses you instantiate in tight loops — meaningful memory savings.
💡 Pro tips
- Pair dataclasses with Pydantic for API boundaries. Dataclasses for internal types, Pydantic for validated inputs.
- Use
kw_only=Trueon dataclasses with more than 4 fields. Caller code stays readable. - For exhaustive matches on enum or Literal types, the
typing-extensionsassert_nevertrick gives compile-time exhaustiveness via mypy/pyright. - Type checkers (mypy, pyright, pyrefly) understand all three features. Configure them on your project to catch misuse early.
- Run
ruff check— many rules nudge you toward these features (e.g. flag mutable defaults). - For old codebases on Python 3.9 or earlier, plan an upgrade. The DX improvement is real.
Conclusion
Three features, all in the standard library, all immediately useful. Match statement collapses
a class of if/elif chains and shines for structured data. @dataclass
deletes 80% of the boilerplate class code most Python projects ship with. Positional/keyword-only
parameters make your APIs both safer and clearer for callers.
Adopt them in your next project. Six months later your old code will feel verbose and you won’t be sure why — that’s the sign the new patterns took.
Related reading
-
7 Python Anti-Patterns That Quietly Kill Your Code
The habits that modern Python’s dataclasses and match statement help eliminate—mutable defaults, bare excepts, and hand-rolled class boilerplate.
-
The 7 Tools That Changed My Python Workflow in 2026
ruff enforces the features in this article and catches unsafe dataclass patterns—the toolchain complement to modern Python syntax.
-
Python Skills You Need Before Touching Machine Learning
The broader Python roadmap—where match statements, dataclasses, and positional parameters fit in the progression toward ML and AI engineering.