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())  # 1000

You 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 is only for identity checks, such as if 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/case syntax for clean pattern matching

These tools are key to writing more efficient, expressive, and maintainable Python programs.

Leave a Reply

Your email address will not be published. Required fields are marked *