Bringing Python 3.10’s Pattern Matching to Python 3.7+ with Patterna
Python 3.10 introduced a powerful new feature: structural pattern matching (the match/case statement), letting us write concise, expressive branching logic much like a switch or algebraic data type match in other languages. Gone are the days of endless if/elif chains – you can now directly match on values, lists, dicts, custom objects, and more, unpacking data right in the case patterns. This is a game-changer for writing clean, readable code when handling complex inputs. But what if you’re stuck on Python 3.7, 3.8, or 3.9? Until now, those older versions have missed out on pattern matching entirely, forcing developers to write verbose checks or nested if statements. Enter Patterna: an open-source library that brings nearly the same pattern-matching syntax to Python 3.7+. With a simple @match decorator, Patterna lets you use case statements just like in Python 3.10 – even on legacy code. In this post, we’ll introduce Patterna and walk through real-world examples (API responses, user command parsing, data validation, etc.) to show how elegantly it solves common Python problems. Why pattern matching is awesome Structural pattern matching is one of Python’s coolest recent features. Instead of writing something like: if response.get("status") == "ok" and "data" in response: data = response["data"] elif response.get("status") == "error" and "message" in response: err = response["message"] else: # default You can now write: match response: case {"status": "ok", "data": data}: # use data case {"status": "error", "message": msg}: # handle error case _: # default case This directly matches both the key and its value, with elegant destructuring. Pattern matching also shines when inspecting sequences or custom objects. For example, a user input list like ["add", "5", "3"] can be matched in one line to extract the operands, rather than checking if cmd[0]=="add" and parsing indices. And it even lets you match classes/tuples with named fields (using data classes or __match_args__). In short, pattern matching makes code more declarative: you say what shape of data you expect, and bind variables right in the pattern. It’s concise, readable, and reduces boilerplate. Languages like Scala, Haskell, and Rust have had these features for years – Python’s match/case is bringing that power to our fingertips. Python < 3.10 missed out… until now Before Python 3.10, there was no native pattern matching. Developers typically used chains of if/elif, dictionary dispatch tables, or third-party hacks (like nested dict.get calls). These solutions work, but they get clunky with many cases or nested data. For example, validating a nested data structure might require lots of manual checks and conversions. Patterna solves this by backporting pattern matching syntax. It provides an easy @match decorator that transforms your function into one that checks each case block under the hood. The great news is that Patterna aims for near-Python-3.10 syntax compatibility, so your match/case code will look familiar. It just works on Python 3.7+ (no special runtime needed beyond installing the library). For anyone working on older codebases or needing to support Python 3.7–3.9, Patterna is a welcome upgrade. Let’s see how to use it in practice. Introducing Patterna Patterna is available on GitHub saadmanrafat/patterna and on PyPI. You can install it with: pip install patterna Then you simply import the match decorator: from patterna import match Now, you can define functions using @match. Inside such a function, you write case patterns and blocks, just like in native Python 3.10. Patterna will try each case in order, and execute the block for the first matching pattern. Use _ as a wildcard/default case. In short, it mimics Python’s pattern matching as closely as possible. Patterna supports matching on: Values (literals, enums, etc.) Sequences (lists, tuples, with unpacking like [head, *tail]) Mappings/dicts (with specific key patterns) Custom classes (for data classes or classes with __match_args__) Wildcards (_) and variable captures in patterns. Under the hood, Patterna inspects the function’s AST to implement matching behavior. But as a user, you just write idiomatic code. Let’s jump into examples. Examples with @match Here are some real-life scenarios where Patterna makes code super clean: 1. Matching simple values A common pattern is handling specific constant values. For example, suppose we have an HTTP status code or a simple command flag. Instead of writing: def status_message(code): if code == 200: return "OK" elif code == 404: return "Not Found" elif code == 500: return "Server Error" else: return "Unknown code" With Patterna, we can write: from patterna import match @match def status_message(code): case 200: return "

Python 3.10 introduced a powerful new feature: structural pattern matching (the match
/case
statement), letting us write concise, expressive branching logic much like a switch or algebraic data type match in other languages. Gone are the days of endless if
/elif
chains – you can now directly match on values, lists, dicts, custom objects, and more, unpacking data right in the case
patterns. This is a game-changer for writing clean, readable code when handling complex inputs.
But what if you’re stuck on Python 3.7, 3.8, or 3.9? Until now, those older versions have missed out on pattern matching entirely, forcing developers to write verbose checks or nested if
statements. Enter Patterna: an open-source library that brings nearly the same pattern-matching syntax to Python 3.7+. With a simple @match
decorator, Patterna lets you use case
statements just like in Python 3.10 – even on legacy code. In this post, we’ll introduce Patterna and walk through real-world examples (API responses, user command parsing, data validation, etc.) to show how elegantly it solves common Python problems.
Why pattern matching is awesome
Structural pattern matching is one of Python’s coolest recent features. Instead of writing something like:
if response.get("status") == "ok" and "data" in response:
data = response["data"]
elif response.get("status") == "error" and "message" in response:
err = response["message"]
else:
# default
You can now write:
match response:
case {"status": "ok", "data": data}:
# use data
case {"status": "error", "message": msg}:
# handle error
case _:
# default case
This directly matches both the key and its value, with elegant destructuring. Pattern matching also shines when inspecting sequences or custom objects. For example, a user input list like ["add", "5", "3"]
can be matched in one line to extract the operands, rather than checking if cmd[0]=="add"
and parsing indices. And it even lets you match classes/tuples with named fields (using data classes or __match_args__
).
In short, pattern matching makes code more declarative: you say what shape of data you expect, and bind variables right in the pattern. It’s concise, readable, and reduces boilerplate. Languages like Scala, Haskell, and Rust have had these features for years – Python’s match/case is bringing that power to our fingertips.
Python < 3.10 missed out… until now
Before Python 3.10, there was no native pattern matching. Developers typically used chains of if
/elif
, dictionary dispatch tables, or third-party hacks (like nested dict.get
calls). These solutions work, but they get clunky with many cases or nested data. For example, validating a nested data structure might require lots of manual checks and conversions.
Patterna solves this by backporting pattern matching syntax. It provides an easy @match
decorator that transforms your function into one that checks each case
block under the hood. The great news is that Patterna aims for near-Python-3.10 syntax compatibility, so your match/case
code will look familiar. It just works on Python 3.7+ (no special runtime needed beyond installing the library).
For anyone working on older codebases or needing to support Python 3.7–3.9, Patterna is a welcome upgrade. Let’s see how to use it in practice.
Introducing Patterna
Patterna is available on GitHub saadmanrafat/patterna and on PyPI. You can install it with:
pip install patterna
Then you simply import the match
decorator:
from patterna import match
Now, you can define functions using @match
. Inside such a function, you write case
patterns and blocks, just like in native Python 3.10. Patterna will try each case
in order, and execute the block for the first matching pattern. Use _
as a wildcard/default case. In short, it mimics Python’s pattern matching as closely as possible.
Patterna supports matching on:
- Values (literals, enums, etc.)
-
Sequences (lists, tuples, with unpacking like
[head, *tail]
) - Mappings/dicts (with specific key patterns)
-
Custom classes (for data classes or classes with
__match_args__
) -
Wildcards (
_
) and variable captures in patterns.
Under the hood, Patterna inspects the function’s AST to implement matching behavior. But as a user, you just write idiomatic code. Let’s jump into examples.
Examples with @match
Here are some real-life scenarios where Patterna makes code super clean:
1. Matching simple values
A common pattern is handling specific constant values. For example, suppose we have an HTTP status code or a simple command flag. Instead of writing:
def status_message(code):
if code == 200:
return "OK"
elif code == 404:
return "Not Found"
elif code == 500:
return "Server Error"
else:
return "Unknown code"
With Patterna, we can write:
from patterna import match
@match
def status_message(code):
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Server Error"
case _:
return "Unknown code"
print(status_message(404)) # Not Found
Here each case
checks the value of code
. The _
case is the default (like else
). This is clear and concise, especially as the number of cases grows. You can match strings, enums, even boolean combinations (though Patterna currently focuses on direct values and patterns).
2. Matching sequences (lists/tuples)
Imagine a simple command-line interface or parser, where commands come in as lists or tuples. For instance, we might have commands like ["add", "3", "4"]
or ["help"]
. Traditionally you'd write index checks, e.g.:
def handle(cmd):
if len(cmd) == 3 and cmd[0] == "add":
a, b = float(cmd[1]), float(cmd[2])
return a + b
elif len(cmd) == 1 and cmd[0] == "help":
return "Available commands: add "
else:
return "Unknown command"
With Patterna’s sequence matching, this becomes straightforward:
@match
def handle(cmd):
case ["add", a, b]:
# a and b are strings from the list; convert to numbers
return float(a) + float(b)
case ["help"]:
return "Usage: add or help "
case _:
return "Unknown command"
print(handle(["add", "3", "4"])) # 7.0
print(handle(["help"])) # Usage: add or help
Each case
pattern is a list with expected length and contents. The variables a
and b
are automatically set to the second and third elements of the list. If a command doesn’t match any pattern, the _
wildcard handles it.
Patterna also supports sequence unpacking with *
. For example, to capture a rest of the list:
@match
def greet(parts):
case ["greet", *names]:
names_str = ", ".join(names)
return f"Hello {names_str}!"
case _:
return "No greeting for you!"
print(greet(["greet", "Alice", "Bob"])) # Hello Alice, Bob!
Here *names
captures any remaining items in the list. Sequence matching is great for CLI args, parsing data tuples, or splitting inputs.
3. Matching dictionaries (API/JSON responses)
A very common scenario is handling JSON data or API responses, where the input is a Python dict with certain keys. Without pattern matching, you might write:
def handle_response(resp):
status = resp.get("status")
if status == "ok" and "data" in resp:
data = resp["data"]
print("Got data:", data)
elif status == "error" and "message" in resp:
print("Error occurred:", resp["message"])
else:
print("Unexpected response:", resp)
Patterna lets you match keys and values directly:
@match
def handle_response(resp):
case {"status": "ok", "data": data}:
print("Success! Data =", data)
case {"status": "error", "message": msg}:
print("Failure. Message =", msg)
case _:
print("Unknown response format:", resp)
handle_response({"status": "ok", "data": [1, 2, 3]})
# prints: Success! Data = [1, 2, 3]
handle_response({"status": "error", "message": "Not found"})
# prints: Failure. Message = Not found
The case {"status": "ok", "data": data}
line means “match any dict that has "status": "ok"
and a "data"
key; bind its value to data
.” You don’t need to check lengths or missing keys manually. This is super handy for request payloads, config dicts, or any structured data you expect in a request/response.
4. Matching custom classes (data objects)
Pattern matching isn’t limited to built-in types. If you have custom classes (especially data classes), you can match on their fields. For example, suppose you have a simple class representing a user or event:
from dataclasses import dataclass
@dataclass
class Message:
type: str
payload: dict
@match
def process_message(msg):
case Message(type="login", payload={"user": user}):
print(f"User logged in: {user}")
case Message(type="logout", payload={"user": user}):
print(f"User logged out: {user}")
case Message(type="error", payload={"code": code, "info": info}):
print(f"Error {code}: {info}")
case _:
print("Unknown message:", msg)
When you call process_message(Message("login", {"user": "alice"}))
, Patterna checks the type
and unpacks payload
according to the patterns. This replaces code like:
if isinstance(msg, Message):
if msg.type == "login":
user = msg.payload.get("user")
print("User logged in:", user)
# ...
It’s much cleaner to declare patterns up front. Patterna will access the instance attributes automatically (assuming they are in __match_args__
or via __dict__
). This example uses Python’s dataclass
for brevity, but any class with simple attributes works similarly.
5. User input routing (bonus example)
Pattern matching is also great for routing or dispatching logic. For example, suppose we’re building a mini-router for user commands or endpoints. If the commands come in as lists or tuples, Patterna makes it easy:
@match
def router(path):
case ["user", "create", username]:
print(f"Creating user {username}")
case ["user", "delete", username]:
print(f"Deleting user {username}")
case ["post", post_id, "comments"]:
print(f"Fetching comments for post {post_id}")
case _:
print("404 Not Found:", path)
router(["user", "create", "alice"]) # Creating user alice
router(["post", 42, "comments"]) # Fetching comments for post 42
router(["invalid", "path"]) # 404 Not Found: ['invalid', 'path']
Here each case
pattern is a path structure. The last one is a fallback 404. This is analogous to how some web frameworks match URL patterns, but inside pure Python logic.
6. Data validation example
You can even use @match
for simple data validation. Suppose you expect data of a certain shape, like a tuple (id, name, age)
. You can validate and extract like this:
@match
def process_record(record):
case (id, name, age) if isinstance(age, int) and age >= 0:
print(f"Valid record: {id}, {name}, {age}")
case (id, name, age):
print(f"Invalid age for record: {id}, {name}, {age}")
case _:
print("Unexpected format:", record)
process_record((1, "Alice", 30)) # Valid record: 1, Alice, 30
process_record((2, "Bob", -5)) # Invalid age for record: 2, Bob, -5
process_record("bad data") # Unexpected format: bad data
(Note: Patterna’s syntax for guards like if
is in alpha or future plans; if not supported yet, you could handle it inside the block. But even without guards, matching the tuple shape (id, name, age)
is very readable.)
Patterna in action: Life-based examples
Let’s frame these examples as real life problems:
-
API response handling: Rather than multiple
if
conditions on a JSON payload, you write clearcase {"status": X, ...}
patterns. -
User command routing: Instead of parsing a command string or list manually, use a match of the form
case ["action", ...]
. - Configuration or data validation: Match on tuple/list formats to ensure you got the data shape you expect.
-
Object dispatch: Instead of
if isinstance(obj, Foo): ... elif isinstance(obj, Bar): ...
, pattern-match classes or use a common interface.
In all cases, Patterna’s @match
makes the code flatter and easier to read. You see all the possible shapes in one place, rather than jumping around nested conditionals.
Contributors are welcome! If you have ideas (for example, adding pattern guards, or integrating with third-party libraries), head to the Patterna GitHub and start a discussion or PR.
Try Patterna and give feedback
Patterna makes it easy and fun to use pattern matching on older Pythons. Give it a try:
- Install it:
pip install patterna
. - Rewrite a small part of your code with
@match
andcase
to see how it looks. - Check out the Patterna GitHub repo for documentation and examples.
- If you run into issues or have feature requests, open an issue on the repo. The author (and community) would love feedback.
Feel Free to Sponsor:
By bringing pattern matching to Python 3.7–3.9, Patterna lets more projects enjoy cleaner code today. We encourage you to play with it, share your examples, and even contribute improvements. Happy matching!
Patterna is free and open-source. If you find it useful, feel free to support development: Donate via Binance