Sharing notes from my ongoing learning journey — what I build, break and understand along the way.
Python Basics – Part 12: Recursion, Sets, and Pattern Matching
Recursion, Sets, Identity, and Pattern Matching in Python
In this part of the Python Basics series, we’ll explore several advanced but fundamental Python topics: recursion, sets, conditional expressions, the identity operator (is), and the new pattern matching (match/case) syntax introduced in Python 3.10.
Each of these concepts deepens your understanding of how Python works under the hood — from how functions call themselves, to how data is compared, and how control structures can be written more elegantly.
Recursion
Recursion is a technique where a function calls itself to solve a smaller version of the same problem.
It’s often seen as an alternative to loops for problems that can be broken down into subproblems of the same kind — such as computing a factorial, traversing trees, or summing elements in a list.
Factorial Using a Loop
def factorial_loop(n):
result = 1
while n > 1:
result *= n
n -= 1
return result
print(factorial_loop(5)) # 120
This is the standard iterative approach: repeat until the number reaches 1.
Factorial Using Recursion
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1)
print(factorial_recursive(5)) # 120
Here’s what happens behind the scenes:
return 5 * factorial_recursive(4)
return 5 * 4 * factorial_recursive(3)
return 5 * 4 * 3 * factorial_recursive(2)
return 5 * 4 * 3 * 2 * factorial_recursive(1)
return 5 * 4 * 3 * 2 * 1
return 120
Every recursive call creates a new stack frame in memory.
When the base case is reached (n <= 1), the calls start returning in reverse order — this is called unwinding the stack.
Recursive solutions are elegant but slower and use more memory than iterative ones for large inputs.
Python’s default recursion limit is 1000, which you can check or modify:import sys print(sys.getrecursionlimit()) # 1000You can increase it with
sys.setrecursionlimit(10000), but this is rarely recommended — it can cause crashes if recursion grows unexpectedly.
Recursive Sum of a List
A simple example of recursion is summing elements of a list.
def recursive_sum(lst):
if len(lst) == 0:
return 0
return lst[0] + recursive_sum(lst[1:])
print(recursive_sum([2, 3, 4, 7])) # 16
Step-by-step:
2 + sum([3, 4, 7])
2 + 3 + sum([4, 7])
2 + 3 + 4 + sum([7])
2 + 3 + 4 + 7 + sum([])
= 16
Each
[1:]slice creates a new list, which increases memory usage.
A more efficient version uses an index parameter instead of slicing:
def recursive_sum_index(lst, i=0):
if i == len(lst):
return 0
return lst[i] + recursive_sum_index(lst, i + 1)
Recursive Line Example
def line(n, char):
if n == 0:
return ''
return char + line(n - 1, char)
print(line(10, '*')) # **********
This works fine for short strings, but concatenating strings recursively is inefficient because strings are immutable.
A simpler and more Pythonic solution is:
def line_simple(n, char):
return char * n
Recursion Limit and Errors
Python raises a RecursionError if a recursive function calls itself too many times:
def recurse():
recurse()
# recurse() # RecursionError: maximum recursion depth exceeded
Tail recursion (a common optimization in other languages) is not supported in Python — each recursive call always adds a new frame to the stack.
Sets
A set is an unordered collection of unique elements.
It’s mutable (can be modified), but unlike lists, sets do not allow duplicates and do not preserve order.
set1 = {1, 2, 3}
print(set1) # {1, 2, 3}
print(type(set1)) # <class 'set'>
Because sets are unordered, they don’t support indexing:
# set1[0] # TypeError
Removing Duplicates from a List
numbers = [3, 5, 7, 3, 5, 2, 9]
unique = list(set(numbers))
print(unique) # [2, 3, 5, 7, 9]
This is the easiest way to remove duplicates — but note that the order may change, because sets are unordered.
Creating and Modifying Sets
set2 = set()
print(type(set2)) # <class 'set'>
set3 = {1, 2, 3}
set3.add(4)
set3.add(5)
print(set3) # {1, 2, 3, 4, 5}
To safely remove an item without an error if it doesn’t exist, use discard() instead of remove():
set3.discard(42) # does nothing
remove()raises a KeyError if the item is not found.discard()silently ignores it.
Set Operations
Sets support powerful mathematical operations:
A = {1, 2, 3}
B = {3, 4, 5}
print(A | B) # union -> {1, 2, 3, 4, 5}
print(A & B) # intersection -> {3}
print(A - B) # difference -> {1, 2}
print(A ^ B) # symmetric difference -> {1, 2, 4, 5}
Set operations are very fast in Python and great for membership tests or data cleanup.
You can also make immutable sets with frozenset():
immutable = frozenset({1, 2, 3})
Conditional Expressions
Conditional expressions are Python’s version of the ternary operator, letting you write concise inline if/else statements.
day = 4
message = "Weekday" if day <= 5 else "Weekend"
print(message) # Weekday
You can also use them inside f-strings:
print(f"Today is a {'workday' if day <= 5 else 'weekend'}.")
This makes code shorter and more readable for simple decisions.
The is Operator (Identity vs Equality)
The is operator checks if two variables refer to the same object in memory,
whereas == checks if their values are equal.
str1 = "Hello"
str2 = "Hello"
print(str1 == str2) # True (same content)
print(str1 is str2) # True (same object due to interning)
For small strings and numbers, Python reuses memory locations (called interning).
But for mutable types, each object is distinct:
list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 == list2) # True
print(list1 is list2) # False
Mutable types (lists, dicts, sets) always have different identities, even with identical values.
list3 = [1, 2, 3]
list4 = list3
print(list3 is list4) # True (same reference)
list3.append(99)
print(list4) # [1, 2, 3, 99]
Use
isonly for identity checks, such asif x is None:— not for value comparisons.
Pattern Matching (match / case)
Starting with Python 3.10, Python introduced structural pattern matching,
which goes beyond simple switch logic.
from random import randint
day = randint(1, 8)
print(day, "->", end=" ")
match day:
case 1:
print("Monday")
case 2:
print("Tuesday")
case 3:
print("Wednesday")
case 4:
print("Thursday")
case 5:
print("Friday")
case 6:
print("Saturday")
case 7:
print("Sunday")
case _:
print("Error")
The underscore _ acts as a wildcard or default case.
Equivalent if/elif chain:
if day == 1:
print("Monday")
elif day == 2:
print("Tuesday")
# ...
else:
print("Error")
Dictionary Mapping Alternative
You can achieve a similar effect using a dictionary:
days = {
1: "Monday",
2: "Tuesday",
3: "Wednesday",
4: "Thursday",
5: "Friday",
6: "Saturday",
7: "Sunday",
}
print(days.get(day, "Error"))
Pattern Matching Beyond switch
match/case is more powerful than a traditional switch because it can unpack data structures:
point = (2, 0)
match point:
case (0, 0):
print("Origin")
case (x, 0):
print(f"On X-axis at {x}")
case (0, y):
print(f"On Y-axis at {y}")
case (x, y):
print(f"Point at ({x}, {y})")
This feature is known as structural pattern matching,
and it can match sequences, objects, and even data classes.
Summary
In this part, we covered several advanced but essential Python concepts:
- How recursion works and when it’s better to use loops
- Using sets to store unique data and perform fast mathematical operations
- Writing concise code with conditional expressions
- Understanding the difference between equality (
==) and identity (is) - Using Python’s modern
match/casesyntax for clean pattern matching
These tools are key to writing more efficient, expressive, and maintainable Python programs.
