Complete Python Programming
Notes — Basic to Advanced
A structured reference covering primitive types, OOP, data structures, recursion, and more — with live examples.
Primitive Types & Basics
1.1 Variables and Data Types
Python has six core primitive data types. Unlike many languages, Python infers types automatically — no declaration needed. Every value in Python is an object, even primitives.
| Type | Description | Example | Notes |
|---|---|---|---|
int | Whole numbers | age = 17 | Unlimited precision |
float | Decimal numbers | price = 19.99 | 64-bit double precision |
bool | True / False | is_student = True | Subclass of int |
str | Text sequence | name = "Alice" | Immutable |
complex | Complex numbers | z = 3 + 4j | Two 64-bit floats |
NoneType | Absence of value | result = None | Singleton object |
Use type(x) to check a variable’s type at runtime, and isinstance(x, int) for type checking in conditions.
# Valid variable names
student_name = "John" # Snake_case (Python convention ✓)
studentAge = 20 # camelCase (works, not Pythonic)
_student = "Jane" # underscore prefix = private convention
student1 = "Bob" # numbers allowed — NOT at start
STUDENT = "Alice" # ALL_CAPS = constants convention
# Invalid variable names (will raise SyntaxError)
# 1student = "John" # ✗ cannot start with a digit
# student-name = "John" # ✗ hyphens not allowed
# class = "Math" # ✗ reserved keyword
# student name = "John" # ✗ spaces not allowed
# Python reserved keywords — never use as identifiers:
# False None True and as assert async await
# break class continue def del elif else except
# finally for from global if import in is
# lambda nonlocal not or pass raise return try
# while with yield
1.2 Operators
| Operator | Name | Example | Result |
|---|---|---|---|
+ | Addition | 5 + 3 | 8 |
- | Subtraction | 5 - 3 | 2 |
* | Multiplication | 5 * 3 | 15 |
/ | Division (float) | 5 / 2 | 2.5 |
// | Floor division | 5 // 2 | 2 |
% | Modulo | 5 % 2 | 1 |
** | Exponentiation | 5 ** 2 | 25 |
PEMDAS order: Parentheses → Exponents (**) → Unary → * / // % → + - → Comparisons → not → and → or
# Operator precedence
result = 5 + 3 * 2 ** 2 # → 5 + 3*4 = 5+12 = 17
result = (5 + 3) * 2 ** 2 # → 8 * 4 = 32
# Assignment shorthand operators
x = 5
x += 3 # x = 8
x -= 2 # x = 6
x *= 4 # x = 24
x /= 3 # x = 8.0 (always float)
x //= 2 # x = 4.0
x %= 3 # x = 1.0
x **= 2 # x = 1.0
1.3 Input and Output
print() sends text to the console. input() reads a string from the user — always returns str, so cast as needed.
name, age = "Alice", 17
# f-strings (Python 3.6+) — preferred modern style
print(f"Hello, {name}! You are {age} years old.")
print(f"Next year you will be {age + 1}.")
print(f"Pi ≈ {3.14159:.2f}") # Pi ≈ 3.14
# Alignment inside f-strings
print(f"{name:>10}") # " Alice" right-align
print(f"{name:<10}") # "Alice " left-align
print(f"{name:^10}") # " Alice " center
print(f"{age:03d}") # "017" zero-pad
# sep and end parameters
print("apple", "banana", sep=", ") # apple, banana
print("Loading", end="...") # no newline
print("Done!") # Loading...Done!
# Older styles (still valid)
print("Hello, {}!".format(name)) # .format()
print("Hello, %s!" % name) # % formatting
name = input("Enter your name: ") # always str
age = int(input("Enter your age: ")) # cast to int
height = float(input("Enter your height: ")) # cast to float
# Multiple values on one line
x, y = input("Enter two numbers: ").split()
x, y = int(x), int(y)
print(f"Sum: {x + y}")
# Validated input with try/except
try:
age = int(input("Enter age: "))
if not 0 <= age <= 150:
print("Invalid age!")
else:
print(f"Valid age: {age}")
except ValueError:
print("Please enter a valid number!")
1.4 Type Conversion
# Implicit: int + float → float
z = 5 + 2.5 # 7.5
# int()
int(3.14) # 3 (truncates, does NOT round)
int("42") # 42
int(True) # 1
int("101", 2) # 5 (binary string → decimal)
# float()
float(5) # 5.0
float("inf") # infinity
float("nan") # Not a Number
# bool() — falsy values:
bool(0) # False
bool("") # False
bool([]) # False
bool(None)# False
# Everything else is truthy
# Rounding
round(3.14159, 2) # 3.14
round(2.5) # 2 ← banker's rounding (rounds to even!)
Using Objects
2.1 Everything is an Object
In Python, even numbers and strings are full objects with methods. You can inspect any object with dir(x) or check its type with x.__class__.
Methods are called with dot notation: object.method(args). Most string methods return a new string since strings are immutable.
2.2 String Methods
.upper() .lower() .title() .swapcase().strip() .lstrip() .rstrip() .center(n).find() returns -1 if missing; .index() raises an error.split(sep) → list; sep.join(list) → string.replace(old, new) — returns new string, doesn't mutate.isalpha() .isdigit() .isalnum() .isspace()text = " Hello, World! "
# Case
text.upper() # " HELLO, WORLD! "
text.lower() # " hello, world! "
text.title() # " Hello, World! "
# Trim
text.strip() # "Hello, World!"
text.lstrip() # "Hello, World! "
text.rstrip() # " Hello, World!"
# Search
text.find("World") # 9 (index; -1 if not found)
text.index("World") # 9 (raises ValueError if missing)
text.count("o") # 2
text.startswith(" ") # True
text.endswith("!") # True ← on stripped version use .strip() first
# Replace & split
text.replace("World", "Python") # " Hello, Python! "
words = text.split(",") # [" Hello", " World! "]
parts = text.split() # ["Hello,", "World!"] (any whitespace)
# Join (inverse of split)
"-".join(["a", "b", "c"]) # "a-b-c"
text = "Hello, World!"
text[0:5] # "Hello"
text[7:12] # "World"
text[:5] # "Hello" (start defaults to 0)
text[7:] # "World!" (stop defaults to end)
text[-6:-1] # "World" (negative from end)
text[::2] # "Hlo ol!" (every 2nd char)
text[::-1] # "!dlroW ,olleH" (reverse!)
# Strings are IMMUTABLE — you cannot do text[0] = "J"
# Instead create a new string:
text = "J" + text[1:] # "Jello, World!"
2.3 Math Library
import math
# Constants
math.pi # 3.14159...
math.e # 2.71828...
math.inf # infinity
# Rounding
math.ceil(3.2) # 4 (always up)
math.floor(3.8) # 3 (always down)
math.trunc(3.7) # 3 (towards zero)
# Powers & roots
math.sqrt(16) # 4.0
math.pow(2, 3) # 8.0
math.exp(2) # e² ≈ 7.389
math.log(100, 10) # 2.0 (log base 10)
# Trigonometry (radians!)
math.sin(math.pi/2) # 1.0
math.cos(0) # 1.0
math.radians(180) # π
math.degrees(math.pi) # 180.0
# Misc
math.factorial(5) # 120
math.gcd(12, 18) # 6
import random
random.random() # float in [0.0, 1.0)
random.randint(1, 10) # int in [1, 10] inclusive
random.randrange(1, 10, 2) # odd number: 1,3,5,7,9
my_list = [1, 2, 3, 4, 5]
random.choice(my_list) # one random element
random.sample(my_list, k=3) # 3 unique elements (no repeats)
random.shuffle(my_list) # shuffle in-place
# Reproducibility
random.seed(42) # same sequence every run
print(random.random()) # always same value with this seed
Boolean Expressions & if Statements
3.1 Comparison & Logical Operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
== | Equal to | 5 == 5 | True |
!= | Not equal | 5 != 3 | True |
< | Less than | 3 < 5 | True |
> | Greater than | 5 > 3 | True |
<= | Less or equal | 5 <= 5 | True |
>= | Greater or equal | 3 >= 5 | False |
and | Both true | True and True | True |
or | Either true | False or True | True |
not | Negate | not False | True |
Python chaining: 1 < x < 10 is valid and equivalent to 1 < x and x < 10. Falsy values: False, 0, 0.0, "", [], (), {}, None.
Short-circuit evaluation: False and f() never calls f(). True or f() never calls f() either. Useful for efficiency and avoiding errors.
3.2 if / elif / else
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"
print(f"Grade: {grade}") # Grade: B
# Ternary (conditional expression) — one-liner
status = "adult" if score >= 18 else "minor"
# Nested conditions → prefer logical operators
age, has_license = 25, True
if age >= 18 and has_license:
print("Can drive")
elif age >= 18:
print("Get a licence first")
else:
print("Too young to drive")
# Leap year (elegant boolean expression)
year = 2024
is_leap = (year % 4 == 0) and (year % 100 != 0 or year % 400 == 0)
print(f"{year} leap year: {is_leap}") # True
Iteration (Loops)
4.1 while Loops
Use while when you don't know the number of iterations upfront — e.g. waiting for valid input. Always ensure the condition eventually becomes False, or use break.
# 1. Input validation loop
while True:
age = input("Enter age (0-120): ")
if age.isdigit() and 0 <= int(age) <= 120:
age = int(age)
break
print("Invalid — try again")
# 2. Skip even numbers with continue
number = 0
while number < 10:
number += 1
if number % 2 == 0:
continue # skip even
print(f"Odd: {number}")
# 3. Sentinel-controlled: stop on negative input
total, count = 0, 0
n = float(input("Enter number (negative to stop): "))
while n >= 0:
total += n
count += 1
n = float(input("Next number: "))
if count:
print(f"Average: {total / count}")
4.2 for Loops
# range(stop), range(start, stop), range(start, stop, step)
for i in range(5): # 0,1,2,3,4
print(i)
for i in range(2, 7): # 2,3,4,5,6
print(i)
for i in range(10, 0, -2): # 10,8,6,4,2
print(i)
# Iterating collections
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)
# enumerate() — index + value (Pythonic)
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# Dictionary iteration
person = {"name": "Alice", "age": 17}
for key, value in person.items():
print(f"{key}: {value}")
# else on for (runs if no break occurred)
numbers = [2, 4, 6, 8]
for n in numbers:
if n % 2 != 0:
print(f"Odd found: {n}")
break
else:
print("All even!") # ← runs here
4.3 Nested Loops & Patterns
# Multiplication table (5×5)
for i in range(1, 6):
for j in range(1, 6):
print(f"{i*j:4}", end="")
print()
# Output:
# 1 2 3 4 5
# 2 4 6 8 10
# ...
# Growing star triangle
for i in range(1, 6):
print("*" * i)
# Centred diamond
for i in range(5):
print(" " * (4 - i) + "*" * (2*i + 1))
4.4 Common Loop Patterns
numbers = [1, 2, 3, 4, 5]
# Accumulator
total = sum(n**2 for n in numbers) # 55
# Filter (list comprehension)
evens = [n for n in numbers if n % 2 == 0] # [2, 4]
# Max (manual + built-in)
max_val = numbers[0]
for n in numbers[1:]:
if n > max_val:
max_val = n
# or simply: max_val = max(numbers)
# Count vowels
text = "Hello, how are you today?"
count = sum(1 for c in text.lower() if c in "aeiou")
# all() / any()
all_positive = all(n > 0 for n in numbers) # True
any_even = any(n % 2 == 0 for n in numbers) # True
Writing Classes
5.1 Class Definition Basics
self.name = value.ClassName.var.__str__ __add__ __len__ etc. — operator overloading.class Dog:
species = "Canis familiaris" # class variable (shared)
dog_count = 0
def __init__(self, name, age):
self.name = name # instance variable
self.age = age
Dog.dog_count += 1
def bark(self):
return f"{self.name} says Woof!"
def get_human_years(self):
return self.age * 7
def __str__(self):
return f"{self.name} ({self.age} yrs)"
def __repr__(self):
return f"Dog('{self.name}', {self.age})"
buddy = Dog("Buddy", 3)
max_ = Dog("Max", 5)
print(buddy.bark()) # Buddy says Woof!
print(buddy.get_human_years()) # 21
print(str(buddy)) # Buddy (3 yrs)
print(Dog.dog_count) # 2
5.2 Properties — Getters & Setters
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
return self._name.title() # auto-format
@name.setter
def name(self, value):
if not isinstance(value, str) or len(value) < 2:
raise ValueError("Invalid name")
self._name = value
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not 0 <= value <= 150:
raise ValueError("Age out of range")
self._age = value
@property # read-only computed
def is_adult(self):
return self._age >= 18
p = Person("alice", 25)
print(p.name) # Alice ← auto title-cased
p.age = 30 # uses setter
print(p.is_adult) # True
# p.age = 999 # ← raises ValueError
5.3 Special (Dunder) Methods
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): return f"({self.x}, {self.y})"
def __repr__(self): return f"Vector({self.x}, {self.y})"
def __add__(self, o): return Vector(self.x + o.x, self.y + o.y)
def __sub__(self, o): return Vector(self.x - o.x, self.y - o.y)
def __mul__(self, s): return Vector(self.x * s, self.y * s)
def __rmul__(self, s):return self.__mul__(s) # 3 * v
def __eq__(self, o): return self.x == o.x and self.y == o.y
def __abs__(self): return (self.x**2 + self.y**2) ** 0.5
def __len__(self): return 2
def __iter__(self): yield self.x; yield self.y
def __bool__(self): return self.x != 0 or self.y != 0
def __call__(self, s):return Vector(self.x * s, self.y * s)
v1, v2 = Vector(2, 3), Vector(1, 4)
print(v1 + v2) # (3, 7)
print(3 * v1) # (6, 9)
print(abs(v1)) # 3.605...
print(list(v1)) # [2, 3]
print(bool(Vector(0,0)))# False
5.4 Encapsulation & Class / Static Methods
class BankAccount:
interest_rate = 0.02 # class variable
account_count = 0
def __init__(self, owner, balance=0):
self._owner = owner # "protected" (by convention)
self._balance = balance
self.__pin = None # name-mangled → _BankAccount__pin
BankAccount.account_count += 1
# ---------- instance methods ----------
def deposit(self, amount):
if self._validate(amount):
self._balance += amount
def get_balance(self, pin):
if pin == self.__pin:
return self._balance
# ---------- @classmethod — operates on the class ----------
@classmethod
def set_rate(cls, rate):
cls.interest_rate = rate
@classmethod
def from_string(cls, s): # alternative constructor
owner, balance = s.split("-")
return cls(owner, float(balance))
# ---------- @staticmethod — utility, no self/cls ----------
@staticmethod
def _validate(amount):
return 0 < amount < 1_000_000
@property
def owner(self):
return self._owner # read-only
acc = BankAccount.from_string("Alice-500")
print(acc.owner) # Alice
BankAccount.set_rate(0.025) # changes for ALL accounts
Arrays (Python Lists)
6.1 Creating & Accessing Lists
Python lists are ordered, mutable, and allow duplicates. They can hold mixed types and grow/shrink dynamically. Indexing is zero-based; negative indices count from the end.
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True]
chars = list("hello") # ['h','e','l','l','o']
rng = list(range(5)) # [0, 1, 2, 3, 4]
# List comprehensions (Pythonic + fast)
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
# Indexing
fruits = ["apple", "banana", "cherry", "date"]
fruits[0] # "apple"
fruits[-1] # "date"
# Slicing [start:stop:step]
fruits[1:3] # ["banana", "cherry"]
fruits[:2] # ["apple", "banana"]
fruits[-2:] # ["cherry", "date"]
fruits[::-1] # reversed copy
# Modifying
fruits[1] = "blueberry"
fruits[1:3] = ["grape", "kiwi"] # replace a slice
6.2 List Methods
| Method | Action | Returns |
|---|---|---|
.append(x) | Add x at end | None |
.insert(i, x) | Insert x at index i | None |
.extend(iterable) | Add all items from iterable | None |
.pop(i) | Remove & return item at i (default last) | item |
.remove(x) | Remove first occurrence of x | None |
.index(x) | Find first index of x | int |
.count(x) | Count occurrences of x | int |
.sort() | Sort in-place | None |
.reverse() | Reverse in-place | None |
.copy() | Shallow copy | list |
.clear() | Remove all elements | None |
6.3 List Algorithms
# --- Linear Search O(n) ---
def linear_search(arr, target):
for i, val in enumerate(arr):
if val == target:
return i
return -1
# --- Bubble Sort O(n²) ---
def bubble_sort(arr):
a = arr.copy()
for i in range(len(a) - 1):
swapped = False
for j in range(len(a) - 1 - i):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
swapped = True
if not swapped:
break # already sorted
return a
# --- Binary Search O(log n) — list must be sorted! ---
def binary_search(arr, target):
lo, hi = 0, len(arr) - 1
while lo <= hi:
mid = (lo + hi) // 2
if arr[mid] == target: return mid
elif arr[mid] < target: lo = mid + 1
else: hi = mid - 1
return -1
nums = [11, 12, 22, 25, 34, 64, 90]
print(binary_search(nums, 25)) # 3
print(binary_search(nums, 30)) # -1
ArrayList (Dynamic Lists)
Python Lists as Dynamic Arrays
Python lists are already dynamic arrays — they resize automatically. Internally, Python over-allocates capacity in chunks, so append() is O(1) amortised. Inserting or removing from the beginning is O(n) because every element must shift.
| Operation | Time Complexity | Note |
|---|---|---|
| Access by index | O(1) | Direct memory lookup |
| Append (end) | O(1) amortised | Occasional resize is O(n) |
| Insert at index i | O(n) | Elements after i must shift |
| Remove by index | O(n) | Elements after shift left |
| Search (unsorted) | O(n) | Linear scan |
| Search (sorted) | O(log n) | Binary search |
import sys
# Lists grow dynamically
lst = []
for i in range(8):
lst.append(i)
print(f"len={len(lst):2} bytes={sys.getsizeof(lst)}")
# Size jumps in chunks — not every append triggers a resize
# For numeric performance: array module (homogeneous, C-style)
from array import array
int_arr = array('i', [1, 2, 3, 4, 5]) # 'i' = signed int
flt_arr = array('f', [1.5, 2.7]) # 'f' = float
import sys
lst_5 = [1, 2, 3, 4, 5]
arr_5 = array('i', [1, 2, 3, 4, 5])
print(f"list : {sys.getsizeof(lst_5)} bytes")
print(f"array : {sys.getsizeof(arr_5)} bytes") # much smaller
# array is more memory-efficient but only one type allowed
For heavy numerical work, consider NumPy arrays — vectorised operations, broadcasting, and much faster than Python lists for math.
2D Arrays (Nested Lists)
8.1 Creating 2D Lists
Common trap: [[0]*cols]*rows creates references to the same row — modifying one row modifies all. Always use a list comprehension: [[0]*cols for _ in range(rows)]
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
# Safe creation
rows, cols = 3, 4
grid = [[0] * cols for _ in range(rows)]
# Access
val = matrix[1][2] # 6 (row 1, col 2)
matrix[2][1] = 10 # modify element
# Dimensions
r = len(matrix) # 3
c = len(matrix[0]) # 3
# Get column (no built-in)
col2 = [row[2] for row in matrix] # [3, 6, 9]
# Iterate all elements
for i, row in enumerate(matrix):
for j, val in enumerate(row):
print(f"[{i}][{j}] = {val}")
# Flatten to 1D
flat = [v for row in matrix for v in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
8.2 Matrix Operations
def add_matrices(A, B):
rows, cols = len(A), len(A[0])
return [[A[i][j] + B[i][j] for j in range(cols)] for i in range(rows)]
def multiply_matrices(A, B):
rA, cA, cB = len(A), len(A[0]), len(B[0])
R = [[0]*cB for _ in range(rA)]
for i in range(rA):
for j in range(cB):
for k in range(cA):
R[i][j] += A[i][k] * B[k][j]
return R
# Transpose (swap rows/cols) — Pythonic with zip
def transpose(m):
return [list(row) for row in zip(*m)]
# Rotate 90° clockwise — Pythonic
def rotate_cw(m):
return [list(row[::-1]) for row in zip(*m)]
# Is symmetric?
def is_symmetric(m):
n = len(m)
return all(m[i][j] == m[j][i] for i in range(n) for j in range(i+1, n))
A = [[1,2],[3,4]]
B = [[5,6],[7,8]]
print(add_matrices(A, B)) # [[6,8],[10,12]]
print(multiply_matrices(A, B)) # [[19,22],[43,50]]
print(transpose(A)) # [[1,3],[2,4]]
Inheritance
9.1 Basic Inheritance
Inheritance lets a child class reuse code from a parent class. Use super() to call the parent's methods. Override a method by redefining it in the child class — Python automatically calls the most specific version.
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
return f"{self.name} makes a sound"
def eat(self, food):
return f"{self.name} eats {food}"
class Dog(Animal):
def __init__(self, name, age, breed):
super().__init__(name, age) # call parent __init__
self.breed = breed
def speak(self): # override
return f"{self.name} says Woof!"
def fetch(self, item):
return f"{self.name} fetches the {item}"
class Cat(Animal):
def __init__(self, name, age, color):
super().__init__(name, age)
self.color = color
def speak(self):
return f"{self.name} says Meow!"
# Polymorphism in action
animals = [Dog("Buddy", 3, "Retriever"), Cat("Whiskers", 5, "orange")]
for a in animals:
print(a.speak()) # each calls its own speak()
print(a.eat("treats")) # inherited from Animal
print(isinstance(animals[0], Animal)) # True
9.2 Abstract Base Classes
from abc import ABC, abstractmethod
import math
class Shape(ABC):
@abstractmethod
def area(self): pass
@abstractmethod
def perimeter(self): pass
def description(self): # concrete shared method
return f"I am a {self.__class__.__name__}"
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r ** 2
def perimeter(self): return 2 * math.pi * self.r
class Rectangle(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
def perimeter(self): return 2 * (self.w + self.h)
# Shape() ← TypeError: Can't instantiate abstract class
c = Circle(5)
r = Rectangle(4, 6)
print(f"Circle area: {c.area():.2f}") # 78.54
print(f"Rectangle area: {r.area()}") # 24
print(c.description()) # I am a Circle
9.3 super() Method Chaining
class Person:
def __init__(self, name, age):
self.name, self.age = name, age
def introduce(self):
return f"Hi, I'm {self.name}, {self.age} years old"
class Employee(Person):
def __init__(self, name, age, emp_id, dept):
super().__init__(name, age)
self.emp_id = emp_id
self.dept = dept
def introduce(self):
return super().introduce() + f". I work in {self.dept}"
class Manager(Employee):
def __init__(self, name, age, emp_id, dept, team_size):
super().__init__(name, age, emp_id, dept)
self.team_size = team_size
def introduce(self):
return super().introduce() + f" and manage {self.team_size} people"
mgr = Manager("Alice", 35, "E123", "Engineering", 8)
print(mgr.introduce())
# Hi, I'm Alice, 35 years old. I work in Engineering and manage 8 people
Recursion
10.1 The Two Rules of Recursion
sys.setrecursionlimit).@functools.lru_cache.10.2 Classic Recursion Examples
def factorial(n):
if n <= 1: # base case
return 1
return n * factorial(n - 1) # recursive case
# Call trace for factorial(5):
# factorial(5) = 5 * factorial(4)
# = 5 * 4 * factorial(3)
# = 5 * 4 * 3 * factorial(2)
# = 5 * 4 * 3 * 2 * factorial(1)
# = 5 * 4 * 3 * 2 * 1 = 120
print(factorial(5)) # 120
print(factorial(10)) # 3628800
from functools import lru_cache
# 1. Naive recursive — O(2^n) — very slow for large n
def fib_naive(n):
if n <= 1: return n
return fib_naive(n-1) + fib_naive(n-2)
# 2. Memoized — O(n) — fast with caching
@lru_cache(maxsize=None)
def fib_memo(n):
if n <= 1: return n
return fib_memo(n-1) + fib_memo(n-2)
# 3. Iterative — O(n) time, O(1) space — best for single values
def fib_iter(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
print(fib_iter(35)) # 9227465 (instant)
# fib_naive(35) # noticeably slow!
# --- Recursive Binary Search ---
def bin_search(arr, target, lo=0, hi=None):
if hi is None: hi = len(arr) - 1
if lo > hi: return -1
mid = (lo + hi) // 2
if arr[mid] == target: return mid
if arr[mid] < target: return bin_search(arr, target, mid+1, hi)
return bin_search(arr, target, lo, mid-1)
# --- Palindrome ---
def is_palindrome(s):
s = s.lower().replace(" ", "")
if len(s) <= 1: return True
if s[0] != s[-1]: return False
return is_palindrome(s[1:-1])
print(is_palindrome("A man a plan a canal Panama")) # True
# --- Tower of Hanoi ---
def hanoi(n, src, tgt, aux):
if n == 1:
print(f"Move disk 1: {src} → {tgt}")
return
hanoi(n-1, src, aux, tgt)
print(f"Move disk {n}: {src} → {tgt}")
hanoi(n-1, aux, tgt, src)
hanoi(3, 'A', 'C', 'B') # Prints 7 moves
# Pattern 1: Tail recursion (last op is the call)
def tail_factorial(n, acc=1):
if n <= 1: return acc
return tail_factorial(n-1, n * acc) # no work after call
# Pattern 2: Divide & conquer power — O(log n)
def fast_power(base, exp):
if exp == 0: return 1
half = fast_power(base, exp // 2)
return half * half if exp % 2 == 0 else base * half * half
# Pattern 3: Mutual recursion
def is_even(n): return True if n == 0 else is_odd(n-1)
def is_odd(n): return False if n == 0 else is_even(n-1)
print(is_even(4)) # True
print(fast_power(2, 10)) # 1024
When to use recursion: Natural for tree/graph traversal, divide-and-conquer, and problems defined recursively (like Fibonacci). Prefer iteration when the depth could be large or performance is critical. Python's default recursion limit is 1000 — check with import sys; sys.getrecursionlimit().
