Bytebook · PYTHON
Python Programming
Python from first print() to real programs: data types, control flow, lists, dictionaries, functions, files, exceptions, OOP and generators.
Chapter 1 · Python Basics
Python Introduction
Why Python is everywhere, how it runs your code, indentation, comments, print(), and your first programs.
Python was created by Guido van Rossum and released in 1991. It was designed to be readable: code looks almost like English, and there is usually one obvious way to do something. Today Python is the language of data science, machine learning, automation, web back-ends, scripting and competitive programming.
print("Hello, Python!")
print("2 + 3 =", 2 + 3)
print("Python" * 3)Hello, Python! 2 + 3 = 5 PythonPythonPython
Compare that with C: no #include, no main, no semicolons, no types to declare. The program simply runs from top to bottom.
How Python runs your code
C is compiled ahead of time into machine code. Python is interpreted: the python program reads your .py file, compiles it to bytecode, and the Python virtual machine executes that bytecode line by line.
| C | Python | |
|---|---|---|
| Typing | static: int x |
dynamic: x = 5, types checked at run time |
| Execution | compiled to machine code | interpreted (bytecode + virtual machine) |
| Speed | very fast | slower, but fast enough for most tasks |
| Memory | manual (malloc/free) |
automatic (garbage collected) |
| Blocks | { } braces |
indentation |
| Lines of code for a task | more | far fewer |
python3 hello.py # run a file
python3 # start the interactive shell (REPL)In the interactive shell, each line runs as soon as you press Enter — great for experiments:
>>> 7 * 6
42
>>> "kalvium".upper()
'KALVIUM'Indentation is syntax
Python uses indentation (4 spaces by convention) to mark blocks. Wrong indentation is an error, not just bad style.
marks = 82
if marks >= 40:
print("Pass") # inside the if block
print("Well done!")
print("Result printed") # back outside the blockPass Well done! Result printed
Comments
# A single-line comment starts with a hash.
"""
A string on its own line is often used as a
multi-line comment or documentation (a docstring).
"""print() in detail
print("A", "B", "C") # separated by spaces
print("A", "B", "C", sep="-") # custom separator
print("no newline", end=" ... ") # custom ending
print("continued")
name, score = "Asha", 93.456
print(f"{name} scored {score:.1f}%") # f-string with formatting
print("Tab\tand\nnewline")A B C A-B-C no newline ... continued Asha scored 93.5% Tab and newline
f"..." strings (f-strings) put expressions inside { }. They are the modern way to format output and you will use them constantly.
Python is everywhere
| Area | Popular tools |
|---|---|
| Data analysis | pandas, NumPy, Jupyter |
| Machine learning / AI | scikit-learn, PyTorch, TensorFlow |
| Web back-ends | Django, Flask, FastAPI |
| Automation & scripting | os, pathlib, requests, Selenium |
| Competitive programming | built-in big integers, lists, dicts |
Chapter 2 · Python Basics
Variables & Data Types
Dynamic typing, the core types (int, float, str, bool, None), type() and conversions, multiple assignment, and how Python variables are really references.
A variable is created the moment you assign to it. There is no declaration and no type keyword — the value carries the type.
age = 19 # int
cgpa = 8.75 # float
name = "Asha" # str
is_enrolled = True # bool
nothing = None # NoneType: "no value"
print(type(age), type(cgpa), type(name), type(is_enrolled), type(nothing))
age = "nineteen" # the same name can later hold a different type
print(type(age))<class 'int'> <class 'float'> <class 'str'> <class 'bool'> <class 'NoneType'> <class 'str'>
Core types
| Type | Example | Notes |
|---|---|---|
int |
42, -7, 10**30 |
unlimited size — no overflow |
float |
3.14, 2e-3 |
64-bit double precision |
complex |
3 + 4j |
built-in complex numbers |
str |
'hi', "hi", """multi-line""" |
immutable text |
bool |
True, False |
a subclass of int (True == 1) |
NoneType |
None |
the absence of a value |
Python integers never overflow — try a number that would break C's long long:
print(2 ** 100)
print(len(str(2 ** 1000)), "digits in 2^1000")
factorial = 1
for i in range(1, 26):
factorial *= i
print("25! =", factorial)1267650600228229401496703205376 302 digits in 2^1000 25! = 15511210043330985984000000
Converting between types
print(int("45") + 5) # str -> int
print(float("2.5") * 2) # str -> float
print(int(9.99)) # float -> int truncates towards zero
print(round(9.99), round(2.5), round(3.5)) # rounds to the nearest even on .5
print(str(100) + " marks") # int -> str
print(bool(0), bool(""), bool("0"), bool([]), bool(3))50 5.0 9 10 2 4 100 marks False False True False True
Naming rules and conventions
- Letters, digits and underscores; cannot start with a digit; case-sensitive.
- Cannot be a keyword:
if,for,class,def,True,None, … - Convention (PEP 8):
snake_casefor variables and functions,PascalCasefor classes,UPPER_CASEfor constants.
Multiple assignment and swapping
a, b, c = 1, 2, 3
x = y = 0
a, b = b, a # swap without a temporary variable
print(a, b, c, x, y)
first, *rest = [10, 20, 30, 40] # unpacking with a star
print(first, rest)2 1 3 0 0 10 [20, 30, 40]
Variables are references
In C, a variable is a box that holds a value. In Python, a variable is a name that refers to an object. Assignment binds a name to an object; it does not copy the object.
a = [1, 2, 3]
b = a # b refers to the SAME list
b.append(4)
print(a) # a sees the change
print(a is b, id(a) == id(b))
c = list(a) # make a real copy
c.append(5)
print(a, c, a is c)[1, 2, 3, 4] True True [1, 2, 3, 4] [1, 2, 3, 4, 5] False
This matters for mutable objects (lists, dicts, sets). Immutable ones (int, float, str, tuple) can never change in place, so sharing them is always safe.
Chapter 3 · Python Basics
Operators & Input
Arithmetic including // and **, comparison chains, logical and identity operators, input() with type conversion, and formatted output.
Arithmetic operators
| Operator | Meaning | Example | Result |
|---|---|---|---|
+ - * |
add, subtract, multiply | 17 + 5, 17 - 5, 17 * 5 |
22, 12, 85 |
/ |
true division (always float) | 17 / 5 |
3.4 |
// |
floor division | 17 // 5 |
3 |
% |
remainder | 17 % 5 |
2 |
** |
power | 17 ** 5 |
1419857 |
print(17 / 5, 17 // 5, 17 % 5, 17 ** 5)
print(-17 // 5, -17 % 5) # floor division rounds DOWN, so the remainder is non-negative
print(divmod(17, 5)) # quotient and remainder together
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
print(abs(-7.5), pow(2, 10), round(3.14159, 2))3.4 3 2 1419857 -4 3 (3, 2) 0.30000000000000004 False 7.5 1024 3.14
Comparison and logical operators
Comparisons return True or False, and Python lets you chain them like maths:
x = 15
print(10 < x <= 20) # same as 10 < x and x <= 20
print(x == 15 and x % 2 == 1)
print(not x > 100 or x < 0)
print("apple" < "banana", "Zebra" < "apple") # dictionary order; uppercase sorts firstTrue True True True True
Python uses the words and, or, not instead of &&, ||, !. They short-circuit, and they return one of their operands rather than strictly True/False:
name = ""
print(name or "Guest") # empty string is falsy, so "Guest"
print(0 and 10, 5 and 10)Guest 0 10
Identity and membership
| Operator | Checks |
|---|---|
is, is not |
are both names the same object? |
in, not in |
is a value inside a collection or string? |
vowels = "aeiou"
print("e" in vowels, "z" not in vowels)
print(3 in [1, 2, 3])
x = None
print(x is None) # the right way to test for NoneTrue True True True
Augmented assignment
x += 1, x -= 2, x *= 3, x //= 2, x **= 2 … Python has no ++ or -- operators.
Reading input
input() always returns a string. Convert it before doing arithmetic:
name = input("Your name: ")
age = int(input("Your age: "))
print(f"Hi {name}, next year you will be {age + 1}.")Your name: Ravi Your age: 19 Hi Ravi, next year you will be 20.
Read several numbers from one line with split() and map():
a, b, c = map(int, input("Three numbers: ").split())
print("sum =", a + b + c, "max =", max(a, b, c))
numbers = list(map(int, "4 8 15 16 23 42".split()))
print(numbers, "average", sum(numbers) / len(numbers))Three numbers: 12 7 30
sum = 49 max = 30
[4, 8, 15, 16, 23, 42] average 18.0Formatted output
item, qty, price = "Notebook", 3, 45.5
print(f"{item:<12}{qty:>4}{price * qty:>10.2f}") # widths and alignment
print(f"{'Total':<12}{'':>4}{price * qty:>10.2f}")
print(f"{1234567.891:,.2f}") # thousands separator
print(f"{0.4567:.1%}") # percentage
print(f"{255:b} {255:o} {255:x}") # binary, octal, hex
print("{} scored {}".format("Asha", 91)) # older str.format styleNotebook 3 136.50 Total 136.50 1,234,567.89 45.7% 11111111 377 ff Asha scored 91
Chapter 4 · Python Basics
Strings
Indexing and negative indexes, slicing, immutability, the most useful string methods, searching, splitting and joining, and classic string programs.
A string is an immutable sequence of characters. Single, double or triple quotes all work.
s = "Kalvium"
print(len(s), s[0], s[-1], s[2]) # negative indexes count from the end
print(s[0:3], s[3:], s[:4], s[-3:]) # slices: start included, end excluded
print(s[::-1]) # step -1 reverses
print(s[::2]) # every second character7 K m l Kal vium Kalv ium muivlaK Klim
s[1:4] is "alv".Strings are immutable
You cannot change a character in place; build a new string instead:
s = "python"
# s[0] = "P" # TypeError: 'str' object does not support item assignment
s = "P" + s[1:]
print(s)Python
Essential string methods
| Method | Does | Example result |
|---|---|---|
upper(), lower(), title(), capitalize() |
change case | "hi there".title() → Hi There |
strip(), lstrip(), rstrip() |
remove whitespace (or given characters) | " hi ".strip() → hi |
split(sep) |
split into a list | "a,b,c".split(",") → ['a', 'b', 'c'] |
sep.join(list) |
join a list into a string | "-".join(["a","b"]) → a-b |
replace(old, new) |
replace all occurrences | "aaa".replace("a","b") → bbb |
find(sub) / index(sub) |
position of sub (find gives −1 if missing) |
"hello".find("l") → 2 |
count(sub) |
occurrences | "banana".count("a") → 3 |
startswith, endswith |
prefix / suffix tests | |
isdigit, isalpha, isalnum, isspace, isupper |
character class tests |
email = " Asha.Rao@Example.COM "
clean = email.strip().lower()
user, domain = clean.split("@")
print(clean, user, domain)
print(clean.endswith(".com"), clean.count("a"), clean.find("rao"))
words = "data structures and algorithms".split()
print(words)
print(" ".join(w.capitalize() for w in words))
print("-".join(["2026", "11", "14"]))asha.rao@example.com asha.rao example.com True 4 5 ['data', 'structures', 'and', 'algorithms'] Data Structures And Algorithms 2026-11-14
Looping over strings
text = "Hello, World 2026!"
letters = digits = spaces = others = 0
for ch in text:
if ch.isalpha():
letters += 1
elif ch.isdigit():
digits += 1
elif ch.isspace():
spaces += 1
else:
others += 1
print(letters, digits, spaces, others)
for i, ch in enumerate("abc"):
print(i, ch, ord(ch)) # ord gives the Unicode code; chr goes back
print(chr(65), chr(0x20B9))10 4 2 2 0 a 97 1 b 98 2 c 99 A ₹
Classic string programs
def is_palindrome(s):
cleaned = "".join(c.lower() for c in s if c.isalnum())
return cleaned == cleaned[::-1]
def count_vowels(s):
return sum(1 for c in s.lower() if c in "aeiou")
def is_anagram(a, b):
return sorted(a.replace(" ", "").lower()) == sorted(b.replace(" ", "").lower())
def char_frequency(s):
freq = {}
for c in s:
freq[c] = freq.get(c, 0) + 1
return freq
print(is_palindrome("A man, a plan, a canal: Panama"))
print(count_vowels("Object Oriented Programming"))
print(is_anagram("Listen", "Silent"), is_anagram("Dormitory", "Dirty room"))
print(char_frequency("mississippi"))True
9
True True
{'m': 1, 'i': 4, 's': 4, 'p': 2}Chapter 5 · Control Flow
If, Elif, Else & match
Decisions in Python: if/elif/else, nested conditions, the conditional expression, and structural pattern matching with match/case.
marks = int(input("Marks: "))
if marks >= 90:
grade = "O"
elif marks >= 80:
grade = "A+"
elif marks >= 70:
grade = "A"
elif marks >= 60:
grade = "B+"
elif marks >= 40:
grade = "P"
else:
grade = "F"
print("Grade:", grade)Marks: 73
Grade: AKey points:
- A colon
:ends the condition line; the block is the indented lines below. elif(else-if) can repeat;elseis optional and comes last.- Conditions can be any value — Python uses its truthiness.
Nested conditions and combining tests
def leap_year(y):
return (y % 4 == 0 and y % 100 != 0) or y % 400 == 0
for year in (1900, 2000, 2024, 2026):
print(year, "leap" if leap_year(year) else "not leap")
age, has_id = 20, True
if age >= 18:
if has_id:
print("Entry allowed")
else:
print("Show an ID")
else:
print("Too young")1900 not leap 2000 leap 2024 leap 2026 not leap Entry allowed
The conditional expression
value_if_true if condition else value_if_false — Python's version of C's ?::
n = 7
parity = "even" if n % 2 == 0 else "odd"
print(f"{n} is {parity}")
print(max(3, 9), min(3, 9)) # built-ins often replace simple ifs7 is odd 9 3
match / case (Python 3.10+)
match compares a value against patterns — like C's switch, but more powerful. There is no fall-through, so no break is needed.
def describe(command):
match command.split():
case ["quit"] | ["exit"]:
return "Goodbye"
case ["go", direction]:
return f"Moving {direction}"
case ["add", x, y]:
return f"{x} + {y} = {int(x) + int(y)}"
case []:
return "Say something"
case _: # the wildcard: matches anything
return f"Unknown command: {command}"
for cmd in ["go north", "add 4 5", "exit", "dance", ""]:
print(repr(cmd), "->", describe(cmd))'go north' -> Moving north 'add 4 5' -> 4 + 5 = 9 'exit' -> Goodbye 'dance' -> Unknown command: dance '' -> Say something
A simple value match looks just like a switch:
def day_type(day):
match day:
case "Sat" | "Sun":
return "weekend"
case "Mon" | "Tue" | "Wed" | "Thu" | "Fri":
return "weekday"
case _:
return "not a day"
print(day_type("Sun"), day_type("Wed"), day_type("Funday"))weekend weekday not a day
Chapter 6 · Control Flow
Loops: for, while & range
Iterate with for over any sequence, count with range(), repeat with while, control loops with break, continue and the loop else clause, and write the classic loop programs.
for loops
Python's for loops over the items of any iterable — a list, string, range, dictionary, file …
for subject in ["C", "DSA", "Python"]:
print("Studying", subject)
for ch in "abc":
print(ch.upper(), end=" ")
print()Studying C Studying DSA Studying Python A B C
range()
range(start, stop, step) produces numbers from start up to but not including stop:
print(list(range(5))) # 0..4
print(list(range(2, 10, 3))) # 2, 5, 8
print(list(range(10, 0, -2))) # counting down
for i in range(1, 6):
print(f"7 x {i} = {7 * i}")[0, 1, 2, 3, 4] [2, 5, 8] [10, 8, 6, 4, 2] 7 x 1 = 7 7 x 2 = 14 7 x 3 = 21 7 x 4 = 28 7 x 5 = 35
enumerate and zip
names = ["Asha", "Ravi", "Meera"]
marks = [91, 78, 85]
for i, name in enumerate(names, start=1):
print(i, name)
for name, m in zip(names, marks):
print(f"{name:6} {m}")1 Asha 2 Ravi 3 Meera Asha 91 Ravi 78 Meera 85
while loops
Use while when you do not know in advance how many times to repeat:
total = 0
count = 0
n = int(input("Number (0 to stop): "))
while n != 0:
total += n
count += 1
n = int(input("Number (0 to stop): "))
print(f"{count} numbers, sum {total}")Number (0 to stop): 5 Number (0 to stop): 12 Number (0 to stop): -3 Number (0 to stop): 0 3 numbers, sum 14
break, continue and loop-else
breakleaves the loop immediately.continueskips to the next iteration.- A loop's
elseblock runs only if the loop finished withoutbreak— perfect for searches.
def is_prime(n):
if n < 2:
return False
for d in range(2, int(n ** 0.5) + 1):
if n % d == 0:
break # found a divisor
else:
return True # no break: no divisor found
return False
print([n for n in range(1, 40) if is_prime(n)])
for n in range(1, 11):
if n % 3 == 0:
continue
print(n, end=" ")
print()[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] 1 2 4 5 7 8 10
Classic loop programs
# Fibonacci
a, b = 0, 1
fib = []
for _ in range(12):
fib.append(a)
a, b = b, a + b
print("Fibonacci:", fib)
# Sum of digits and reverse
n = 40213
digits = 0
rev = 0
while n > 0:
digits += n % 10
rev = rev * 10 + n % 10
n //= 10
print("digit sum:", digits, "reversed:", rev)
# Armstrong numbers
arm = [n for n in range(1, 1000) if n == sum(int(d) ** len(str(n)) for d in str(n))]
print("Armstrong:", arm)
# GCD with Euclid's algorithm
x, y = 84, 36
while y:
x, y = y, x % y
print("gcd(84, 36) =", x)Fibonacci: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] digit sum: 10 reversed: 31204 Armstrong: [1, 2, 3, 4, 5, 6, 7, 8, 9, 153, 370, 371, 407] gcd(84, 36) = 12
Pattern printing
String multiplication makes patterns much shorter than in C:
n = 5
for i in range(1, n + 1):
print("* " * i)
print()
for i in range(1, n + 1):
print(" " * (n - i) + "*" * (2 * i - 1))
print()
for i in range(1, n + 1):
print(" ".join(str(j) for j in range(1, i + 1)))*
* *
* * *
* * * *
* * * * *
*
***
*****
*******
*********
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5Chapter 7 · Data Structures in Python
Lists
Python's workhorse collection: creating, indexing and slicing lists, adding and removing items, sorting, copying, 2D lists and the list methods you will use daily.
A list is an ordered, mutable collection that can hold items of any type. It is a dynamic array underneath — like C++'s vector.
marks = [78, 92, 65, 88]
mixed = [1, "two", 3.0, True, [5, 6]]
empty = []
print(marks[0], marks[-1], marks[1:3], len(marks))
marks[2] = 70 # lists are mutable
print(marks)
print(mixed[4][1]) # a list inside a list78 88 [92, 65] 4 [78, 92, 70, 88] 6
Adding and removing
| Method | Effect |
|---|---|
append(x) |
add x at the end |
insert(i, x) |
insert x before index i |
extend(iterable) |
add every item of another collection |
remove(x) |
remove the first occurrence of x (error if absent) |
pop() / pop(i) |
remove and return the last item / item at i |
clear() |
remove everything |
del lst[i], del lst[a:b] |
delete by index or slice |
cart = ["pen", "notebook"]
cart.append("eraser")
cart.insert(1, "pencil")
cart.extend(["ruler", "glue"])
print(cart)
cart.remove("glue")
last = cart.pop()
print("popped", last, "->", cart)
del cart[0]
print(cart, "pencil" in cart, cart.index("eraser"))['pen', 'pencil', 'notebook', 'eraser', 'ruler', 'glue'] popped ruler -> ['pen', 'pencil', 'notebook', 'eraser'] ['pencil', 'notebook', 'eraser'] True 2
Sorting and searching
nums = [42, 7, 19, 7, 88, 3]
print(sorted(nums)) # new sorted list; nums unchanged
nums.sort(reverse=True) # sorts in place
print(nums)
print(min(nums), max(nums), sum(nums), nums.count(7))
students = [("Asha", 88), ("Ravi", 95), ("Meera", 88)]
students.sort(key=lambda s: (-s[1], s[0])) # marks descending, then name
print(students)
words = ["banana", "apple", "Cherry", "fig"]
print(sorted(words)) # uppercase sorts before lowercase
print(sorted(words, key=str.lower)) # case-insensitive
print(sorted(words, key=len)) # by length (ties keep their order)[3, 7, 7, 19, 42, 88]
[88, 42, 19, 7, 7, 3]
3 88 166 2
[('Ravi', 95), ('Asha', 88), ('Meera', 88)]
['Cherry', 'apple', 'banana', 'fig']
['apple', 'banana', 'Cherry', 'fig']
['fig', 'apple', 'banana', 'Cherry']Lists are references: copying correctly
a = [1, 2, [3, 4]]
b = a # same list
c = a[:] # shallow copy (also list(a) or a.copy())
import copy
d = copy.deepcopy(a) # copies nested lists too
a[0] = 100
a[2].append(5)
print("a", a)
print("b", b) # sees everything
print("c", c) # own outer list, but shares the inner list
print("d", d) # fully independenta [100, 2, [3, 4, 5]] b [100, 2, [3, 4, 5]] c [1, 2, [3, 4, 5]] d [1, 2, [3, 4]]
2D lists
rows, cols = 3, 4
grid = [[0] * cols for _ in range(rows)] # correct way
grid[1][2] = 9
for row in grid:
print(row)
wrong = [[0] * cols] * rows # three references to ONE row!
wrong[1][2] = 9
print(wrong)
matrix = [[1, 2, 3], [4, 5, 6]]
transpose = [list(col) for col in zip(*matrix)]
print(transpose)[0, 0, 0, 0] [0, 0, 9, 0] [0, 0, 0, 0] [[0, 0, 9, 0], [0, 0, 9, 0], [0, 0, 9, 0]] [[1, 4], [2, 5], [3, 6]]
Using a list as a stack or queue
stack = []
stack.append(1); stack.append(2); stack.append(3)
print(stack.pop(), stack.pop()) # LIFO: 3 2
from collections import deque
queue = deque(["a", "b"])
queue.append("c")
print(queue.popleft(), queue.popleft()) # FIFO: a b — O(1) at both ends3 2 a b
list.pop(0) also works as a queue, but it is O(n) because every element shifts. Use collections.deque for queues.
List operation costs
| Operation | Cost |
|---|---|
lst[i], lst[i] = x, len(lst) |
O(1) |
append, pop() |
O(1) amortised |
insert(0, x), pop(0), remove(x) |
O(n) |
x in lst, index, count |
O(n) |
sort() (Timsort) |
O(n log n) |
Chapter 8 · Data Structures in Python
Tuples & Sets
Immutable tuples for fixed records and packing/unpacking, and sets for uniqueness and fast membership — with union, intersection and difference.
Tuples
A tuple is an ordered, immutable sequence. Use it for fixed groups of values: coordinates, RGB colours, a database row, multiple return values.
point = (3, 4)
rgb = 255, 128, 0 # brackets are optional
single = (42,) # a one-item tuple needs a trailing comma
print(point[0], rgb, type(single), len(rgb))
x, y = point # unpacking
print(f"x={x}, y={y}")
def min_max(values):
return min(values), max(values) # returns a tuple
lo, hi = min_max([7, 2, 9, 4])
print(lo, hi)
# point[0] = 5 # TypeError: tuples are immutable3 (255, 128, 0) <class 'tuple'> 3 x=3, y=4 2 9
| List | Tuple |
|---|---|
[1, 2, 3] |
(1, 2, 3) |
| mutable | immutable |
| many methods | only count and index |
| variable collections | fixed records |
| cannot be a dict key | can be a dict key (if its items are hashable) |
| slightly more memory | slightly less memory |
distances = {("Delhi", "Agra"): 233, ("Mumbai", "Pune"): 149} # tuple keys
print(distances[("Mumbai", "Pune")])
from collections import namedtuple
Student = namedtuple("Student", ["name", "roll", "cgpa"])
s = Student("Asha", 101, 9.1)
print(s.name, s.cgpa, s)149 Asha 9.1 Student(name='Asha', roll=101, cgpa=9.1)
Sets
A set is an unordered collection of unique, hashable items. It is a hash table underneath, so membership tests are O(1) on average.
nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
unique = set(nums)
print(unique, len(unique))
unique.add(7)
unique.discard(100) # no error if missing (remove() would raise KeyError)
print(4 in unique, 8 in unique)
print(sorted(unique))
empty = set() # {} would be an empty DICT{1, 2, 3, 4, 5, 6, 9} 7
True False
[1, 2, 3, 4, 5, 6, 7, 9]Set operations
Sets support the operations from Discrete Mathematics directly:
| Operation | Operator | Method |
|---|---|---|
| Union A ∪ B | a | b |
a.union(b) |
| Intersection A ∩ B | a & b |
a.intersection(b) |
| Difference A − B | a - b |
a.difference(b) |
| Symmetric difference A ⊕ B | a ^ b |
a.symmetric_difference(b) |
| Subset A ⊆ B | a <= b |
a.issubset(b) |
python_club = {"Asha", "Ravi", "Meera", "Kiran"}
dsa_club = {"Ravi", "Kiran", "Zoya"}
print("both clubs: ", sorted(python_club & dsa_club))
print("either club: ", sorted(python_club | dsa_club))
print("only python: ", sorted(python_club - dsa_club))
print("exactly one: ", sorted(python_club ^ dsa_club))
print({"Ravi", "Kiran"} <= dsa_club)both clubs: ['Kiran', 'Ravi'] either club: ['Asha', 'Kiran', 'Meera', 'Ravi', 'Zoya'] only python: ['Asha', 'Meera'] exactly one: ['Asha', 'Meera', 'Zoya'] True
A frozenset is an immutable set, usable as a dictionary key or as an element of another set.
Chapter 9 · Data Structures in Python
Dictionaries
Key–value storage with O(1) lookup: creating and updating dicts, safe access with get, looping over keys, values and items, nesting, counting and grouping.
A dictionary (dict) maps keys to values. Lookup by key is O(1) on average because a dict is a hash table. Since Python 3.7, dicts remember insertion order.
student = {"name": "Asha", "roll": 101, "cgpa": 9.1}
print(student["name"])
student["cgpa"] = 9.3 # update
student["city"] = "Pune" # add a new key
del student["roll"] # remove
print(student)
print(len(student), "city" in student, "roll" in student)Asha
{'name': 'Asha', 'cgpa': 9.3, 'city': 'Pune'}
3 True FalseSafe access
d[key] raises KeyError if the key is missing. get returns a default instead:
prices = {"pen": 10, "notebook": 45}
print(prices.get("pen"), prices.get("eraser"), prices.get("eraser", 0))
stock = prices.setdefault("ruler", 25) # insert only if missing
print(stock, prices)
print(prices.pop("pen"), prices)10 None 0
25 {'pen': 10, 'notebook': 45, 'ruler': 25}
10 {'notebook': 45, 'ruler': 25}Looping
marks = {"Asha": 91, "Ravi": 78, "Meera": 85}
for name in marks: # keys
print(name, end=" ")
print()
print(list(marks.values()), sum(marks.values()))
for name, m in marks.items(): # key-value pairs
print(f"{name:6} {m} {'distinction' if m >= 85 else ''}")
topper = max(marks, key=marks.get)
print("Topper:", topper)
print(sorted(marks.items(), key=lambda kv: kv[1], reverse=True))Asha Ravi Meera
[91, 78, 85] 254
Asha 91 distinction
Ravi 78
Meera 85 distinction
Topper: Asha
[('Asha', 91), ('Meera', 85), ('Ravi', 78)]Counting and grouping
text = "the quick brown fox jumps over the lazy dog the end"
freq = {}
for word in text.split():
freq[word] = freq.get(word, 0) + 1
print(freq["the"], freq["fox"])
from collections import Counter, defaultdict
c = Counter(text.split())
print(c.most_common(2))
by_length = defaultdict(list) # missing keys start as []
for word in set(text.split()):
by_length[len(word)].append(word)
print({k: sorted(v) for k, v in sorted(by_length.items())})3 1
[('the', 3), ('quick', 1)]
{3: ['dog', 'end', 'fox', 'the'], 4: ['lazy', 'over'], 5: ['brown', 'jumps', 'quick']}Nested dictionaries
course = {
"CSE101": {"name": "Programming in C", "credits": 4, "students": ["Asha", "Ravi"]},
"CSE102": {"name": "Data Structures", "credits": 4, "students": ["Meera"]},
}
course["CSE102"]["students"].append("Kiran")
for code, info in course.items():
print(f"{code}: {info['name']} ({info['credits']} credits) - {len(info['students'])} students")CSE101: Programming in C (4 credits) - 2 students CSE102: Data Structures (4 credits) - 2 students
Building and merging
keys = ["a", "b", "c"]
d1 = dict(zip(keys, [1, 2, 3]))
d2 = {"c": 30, "d": 40}
print(d1 | d2) # merge (Python 3.9+); right side wins on clashes
d1.update(d2)
print(d1)
squares = {n: n * n for n in range(1, 6)}
print(squares)
inverted = {v: k for k, v in squares.items()}
print(inverted[16]){'a': 1, 'b': 2, 'c': 30, 'd': 40}
{'a': 1, 'b': 2, 'c': 30, 'd': 40}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
4| Operation | Average cost |
|---|---|
d[k], d[k] = v, k in d, del d[k] |
O(1) |
| loop over all items | O(n) |
Chapter 10 · Data Structures in Python
Comprehensions
Build lists, sets and dictionaries in one readable line, add filters and conditions, nest them, and know when a plain loop is clearer.
A comprehension builds a new collection from an existing one in a single expression:
[expression for item in iterable if condition]nums = [1, 2, 3, 4, 5, 6, 7, 8]
squares = [n * n for n in nums]
evens = [n for n in nums if n % 2 == 0]
labels = ["even" if n % 2 == 0 else "odd" for n in nums] # if-else goes BEFORE for
print(squares)
print(evens)
print(labels)[1, 4, 9, 16, 25, 36, 49, 64] [2, 4, 6, 8] ['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even']
The equivalent loop for evens is four lines:
evens = []
for n in nums:
if n % 2 == 0:
evens.append(n)Set and dictionary comprehensions
words = ["apple", "banana", "avocado", "cherry", "blueberry"]
first_letters = {w[0] for w in words} # set: unique
lengths = {w: len(w) for w in words} # dict
long_words = {w: n for w, n in lengths.items() if n > 6}
print(sorted(first_letters))
print(lengths)
print(long_words)['a', 'b', 'c']
{'apple': 5, 'banana': 6, 'avocado': 7, 'cherry': 6, 'blueberry': 9}
{'avocado': 7, 'blueberry': 9}Nested comprehensions
Read the for clauses left to right, exactly as nested loops:
pairs = [(x, y) for x in range(1, 4) for y in range(1, 4) if x != y]
print(pairs)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [v for row in matrix for v in row]
table = [[r * c for c in range(1, 5)] for r in range(1, 4)]
print(flat)
for row in table:
print(row)[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)] [1, 2, 3, 4, 5, 6, 7, 8, 9] [1, 2, 3, 4] [2, 4, 6, 8] [3, 6, 9, 12]
Generator expressions
Use round brackets to get a generator that produces values on demand instead of building a whole list in memory. Perfect inside sum, max, any and all:
print(sum(n * n for n in range(1, 101))) # no list created
print(any(c.isdigit() for c in "pass1word"))
print(all(len(w) > 2 for w in ["cat", "dog", "ox"]))
print(max((len(w), w) for w in ["pear", "fig", "banana"]))338350 True False (6, 'banana')
Real examples
marks = {"Asha": 91, "Ravi": 38, "Meera": 85, "Kiran": 29}
passed = [name for name, m in marks.items() if m >= 40]
grade = {name: ("A" if m >= 85 else "B" if m >= 40 else "F") for name, m in marks.items()}
primes = [n for n in range(2, 50) if all(n % d for d in range(2, int(n ** 0.5) + 1))]
print(passed)
print(grade)
print(primes)['Asha', 'Meera']
{'Asha': 'A', 'Ravi': 'F', 'Meera': 'A', 'Kiran': 'F'}
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]Chapter 11 · Functions & Modules
Functions
Define reusable functions with def: parameters and return values, default and keyword arguments, *args and **kwargs, scope and the LEGB rule, and docstrings.
def greet(name):
"""Return a friendly greeting.""" # docstring: documents the function
return f"Hello, {name}!"
def area_and_perimeter(length, width):
return length * width, 2 * (length + width) # return several values (a tuple)
print(greet("Asha"))
a, p = area_and_perimeter(7, 3)
print(a, p)
print(greet.__doc__)Hello, Asha! 21 20 Return a friendly greeting.
A function without a return statement returns None.
Arguments: positional, keyword and default
def simple_interest(principal, rate=7.5, years=1):
return principal * rate * years / 100
print(simple_interest(10000)) # defaults used
print(simple_interest(10000, 8)) # positional
print(simple_interest(10000, years=3)) # keyword: skip rate
print(simple_interest(years=2, principal=5000)) # any order with keywords750.0 800.0 2250.0 750.0
*args and **kwargs
*args collects any number of extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict.
def average(*numbers):
return sum(numbers) / len(numbers) if numbers else 0
def make_profile(name, **details):
lines = [f"Name: {name}"] + [f"{k.title()}: {v}" for k, v in details.items()]
return "\n".join(lines)
print(average(4, 8, 15, 16, 23, 42))
print(make_profile("Ravi", course="CSE", year=2, city="Chennai"))
values = [3, 9, 27]
print(average(*values)) # * unpacks a list into arguments18.0 Name: Ravi Course: CSE Year: 2 City: Chennai 13.0
Scope: the LEGB rule
When Python looks up a name, it searches four scopes in order: Local → Enclosing (outer function) → Global (module) → Built-in.
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print("inner sees:", x)
inner()
print("outer sees:", x)
outer()
print("module sees:", x)
counter = 0
def increment():
global counter # needed to ASSIGN to a global
counter += 1
increment(); increment()
print("counter =", counter)inner sees: local outer sees: enclosing module sees: global counter = 2
nonlocal does the same for a variable in an enclosing function. Prefer returning values over modifying globals.
How arguments are passed
Python passes references to objects ("call by object reference"). If a function mutates a mutable argument, the caller sees the change; rebinding the parameter does not affect the caller.
def mutate(lst):
lst.append(99) # changes the caller's list
def rebind(lst):
lst = [0] # only changes the local name
nums = [1, 2]
mutate(nums)
rebind(nums)
print(nums)[1, 2, 99]
Functions are objects
Functions can be stored in variables, passed to other functions and returned from them:
def apply_twice(f, x):
return f(f(x))
def make_multiplier(n):
def multiply(x):
return x * n # remembers n: a closure
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(apply_twice(double, 5), triple(7))
operations = {"+": lambda a, b: a + b, "*": lambda a, b: a * b}
print(operations["*"](6, 7))20 21 42
Type hints
Optional annotations document expected types (Python does not enforce them, but editors and checkers do):
def bmi(weight_kg: float, height_m: float) -> float:
return round(weight_kg / height_m ** 2, 1)
print(bmi(72.5, 1.68))25.7
Chapter 12 · Functions & Modules
Lambda, map/filter & Recursion
Small anonymous functions with lambda, functional tools map, filter, sorted and reduce, and recursive functions with base cases, memoisation and recursion limits.
lambda
A lambda is a one-expression anonymous function: lambda parameters: expression.
square = lambda x: x * x
add = lambda a, b: a + b
print(square(9), add(3, 4))
students = [("Asha", 88), ("Ravi", 95), ("Meera", 79)]
print(sorted(students, key=lambda s: s[1], reverse=True))
print(max(students, key=lambda s: s[1])[0])81 7
[('Ravi', 95), ('Asha', 88), ('Meera', 79)]
RaviUse lambdas for short throwaway functions, usually as a key= argument. If it needs a name or more than one line, write a def.
map, filter and reduce
| Function | Does | Comprehension equivalent |
|---|---|---|
map(f, xs) |
applies f to every item |
[f(x) for x in xs] |
filter(pred, xs) |
keeps items where pred is true |
[x for x in xs if pred(x)] |
functools.reduce(f, xs) |
folds items into one value | a loop with an accumulator |
from functools import reduce
nums = [1, 2, 3, 4, 5, 6]
print(list(map(lambda x: x ** 2, nums)))
print(list(filter(lambda x: x % 2 == 0, nums)))
print(reduce(lambda acc, x: acc * x, nums)) # 6! = 720
print(list(map(int, "10 20 30".split()))) # the classic input idiom
print(list(map(str.upper, ["c", "dsa", "python"])))[1, 4, 9, 16, 25, 36] [2, 4, 6] 720 [10, 20, 30] ['C', 'DSA', 'PYTHON']
map and filter return lazy iterators; wrap them in list() to see all values.
Recursion
Recursion in Python works exactly as in C: a base case and a recursive case.
def factorial(n):
return 1 if n <= 1 else n * factorial(n - 1)
def power(base, exp):
if exp == 0:
return 1
half = power(base, exp // 2)
return half * half * (base if exp % 2 else 1)
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item)) # recurse into sub-lists
else:
result.append(item)
return result
def permutations(s):
if len(s) <= 1:
return [s]
return [s[i] + p for i in range(len(s)) for p in permutations(s[:i] + s[i + 1:])]
print(factorial(20))
print(power(2, 64))
print(flatten([1, [2, [3, 4], 5], [[6]], 7]))
print(permutations("abc"))2432902008176640000 18446744073709551616 [1, 2, 3, 4, 5, 6, 7] ['abc', 'acb', 'bac', 'bca', 'cab', 'cba']
Memoisation with lru_cache
Naive recursive Fibonacci recomputes the same values exponentially many times. functools.lru_cache remembers results, turning it into O(n):
from functools import lru_cache
@lru_cache(maxsize=None)
def fib_fast(n):
return n if n < 2 else fib_fast(n - 1) + fib_fast(n - 2)
calls = 0
def fib_counted(n):
global calls
calls += 1
return n if n < 2 else fib_counted(n - 1) + fib_counted(n - 2)
fib_counted(20)
print("calls for naive fib(20):", calls)
print("fib(90) with lru_cache:", fib_fast(90))calls for naive fib(20): 21891 fib(90) with lru_cache: 2880067194370816120
This is dynamic programming in one decorator line; the idea is explained fully in Dynamic Programming.
Recursion limit
Python limits recursion depth (about 1000 by default) to protect the stack:
import sys
print(sys.getrecursionlimit())
def countdown(n):
return 0 if n == 0 else countdown(n - 1)
print(countdown(500))
print(countdown(5000))1000
0
Traceback (most recent call last):
File "main.py", line 8, in <module>
print(countdown(5000))
~~~~~~~~~^^^^^^
File "main.py", line 5, in countdown
return 0 if n == 0 else countdown(n - 1)
~~~~~~~~~^^^^^^^
File "main.py", line 5, in countdown
return 0 if n == 0 else countdown(n - 1)
~~~~~~~~~^^^^^^^
File "main.py", line 5, in countdown
return 0 if n == 0 else countdown(n - 1)
~~~~~~~~~^^^^^^^
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceededFor deep recursion, rewrite the function with a loop (or, carefully, raise the limit with sys.setrecursionlimit).
Chapter 13 · Functions & Modules
Modules, Packages & the Standard Library
Organise code into modules, import in different ways, use if name == "main", install packages with pip, and tour the standard library: math, random, datetime, os, collections and more.
A module is simply a .py file. Any Python file can be imported by another, giving you its functions, classes and variables.
PI = 3.14159
def circle_area(r):
return PI * r * r
def rectangle_area(l, w):
return l * w
if __name__ == "__main__":
print("This runs only when geometry.py is executed directly")import geometry
from geometry import rectangle_area
import geometry as geo
print(geometry.circle_area(2))
print(rectangle_area(3, 4))
print(geo.PI)12.56636 12 3.14159
(The demo message in geometry.py did not print, because the module was imported, not run.)
Ways to import
| Form | Use as |
|---|---|
import math |
math.sqrt(2) |
import numpy as np |
np.array(...) — an alias |
from math import sqrt, pi |
sqrt(2), pi |
from math import * |
everything — avoid: unclear where names come from |
if name == "main"
Every module has a __name__. When a file is run directly, its __name__ is "__main__"; when it is imported, __name__ is the module's name. Putting test or demo code under this check lets a file work both as a script and as a library.
Packages and pip
A package is a folder of modules (with an __init__.py). Third-party packages are installed from PyPI with pip:
pip install requests # install a package
pip install numpy pandas # several at once
pip list # what is installed
python3 -m venv .venv # create an isolated virtual environmentTour of the standard library
import math, random, datetime, statistics
print(math.sqrt(144), math.factorial(10), math.gcd(84, 36), math.comb(5, 2))
print(round(math.pi, 4), math.ceil(4.1), math.floor(-4.1), math.log2(1024))
random.seed(7) # same "random" numbers on every run
print(random.randint(1, 6), random.choice(["heads", "tails"]))
deck = list(range(1, 11))
random.shuffle(deck)
print(deck, random.sample(range(100), 3))
print(statistics.mean([70, 85, 90]), statistics.median([3, 1, 4, 1, 5]))
today = datetime.date(2026, 11, 14)
exam = datetime.date(2026, 12, 10)
print((exam - today).days, "days to the exam")
print(today.strftime("%d %B %Y, %A"))12.0 3628800 12 10 3.1416 5 -5 10.0 3 heads [8, 6, 4, 3, 9, 5, 10, 2, 1, 7] [64, 27, 4] 81.66666666666667 3 26 days to the exam 14 November 2026, Saturday
from collections import Counter, deque, defaultdict, OrderedDict
import itertools, string
print(Counter("abracadabra").most_common(3))
print(list(itertools.permutations([1, 2, 3], 2)))
print(list(itertools.combinations("ABCD", 2)))
print(list(itertools.accumulate([1, 2, 3, 4]))) # running totals
print(string.ascii_uppercase[:5], string.digits)[('a', 5), ('b', 2), ('r', 2)]
[(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
[('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
[1, 3, 6, 10]
ABCDE 0123456789| Module | Useful for |
|---|---|
math |
maths functions and constants |
random |
random numbers, shuffling, sampling |
datetime, time |
dates, times, timing code |
os, pathlib, shutil |
files, folders, paths |
sys |
command-line arguments, interpreter settings |
collections |
Counter, deque, defaultdict, namedtuple |
itertools |
permutations, combinations, infinite iterators |
json, csv |
reading and writing data formats |
re |
regular expressions |
statistics |
mean, median, mode, standard deviation |
Chapter 14 · Real Programs
File Handling
Read and write text files safely with open() and with, file modes, reading line by line, CSV and JSON files, and working with paths using pathlib.
Opening files with with
The with statement opens a file and closes it automatically, even if an error happens inside the block:
with open("notes.txt", "w") as f: # "w" creates or overwrites
f.write("Line one\n")
f.write("Line two\n")
print("Line three", file=f) # print can write to files too
with open("notes.txt") as f: # default mode is "r" (read text)
print(f.read())
with open("notes.txt", "a") as f: # "a" appends
f.write("Line four\n")
with open("notes.txt") as f:
for number, line in enumerate(f, start=1): # iterate line by line: memory-friendly
print(number, line.rstrip("\n"))Line one Line two Line three 1 Line one 2 Line two 3 Line three 4 Line four
| Mode | Meaning |
|---|---|
"r" |
read (default); error if the file does not exist |
"w" |
write; erases an existing file |
"a" |
append to the end |
"x" |
create; error if the file already exists |
"r+" |
read and write |
"b" |
binary, combined with the others: "rb", "wb" |
Reading methods
with open("poem.txt", "w") as f:
f.write("Roses are red\nViolets are blue\nPython is neat\nAnd so are you\n")
with open("poem.txt") as f:
first = f.readline() # one line
rest = f.readlines() # list of the remaining lines
print(repr(first))
print(rest)
with open("poem.txt") as f:
text = f.read()
print(len(text.split()), "words,", text.count("\n"), "lines")'Roses are red\n' ['Violets are blue\n', 'Python is neat\n', 'And so are you\n'] 13 words, 4 lines
CSV files
The csv module handles commas inside quoted fields and other edge cases:
import csv
rows = [["roll", "name", "marks"], [101, "Asha", 88], [102, "Ravi", 93], [103, "Meera, K", 79]]
with open("marks.csv", "w", newline="") as f:
csv.writer(f).writerows(rows)
with open("marks.csv", newline="") as f:
reader = csv.DictReader(f) # each row becomes a dict
records = list(reader)
for r in records:
print(f"{r['roll']} {r['name']:10} {r['marks']}")
average = sum(int(r["marks"]) for r in records) / len(records)
print(f"Class average: {average:.2f}")101 Asha 88 102 Ravi 93 103 Meera, K 79 Class average: 86.67
JSON files
JSON maps naturally onto Python dicts and lists — the standard format for configuration and web APIs:
import json
profile = {"name": "Asha", "skills": ["C", "Python"], "cgpa": 9.1, "placed": False}
with open("profile.json", "w") as f:
json.dump(profile, f, indent=2)
with open("profile.json") as f:
loaded = json.load(f)
print(loaded["skills"], loaded == profile)
print(json.dumps({"ok": True, "count": 3})) # to a string['C', 'Python'] True
{"ok": true, "count": 3}Paths with pathlib
from pathlib import Path
folder = Path("reports")
folder.mkdir(exist_ok=True)
for term in ("sem1", "sem2"):
(folder / f"{term}.txt").write_text(f"Results for {term}\n")
for p in sorted(folder.glob("*.txt")):
print(p.name, p.suffix, p.stat().st_size, "bytes:", p.read_text().strip())
print(Path("reports/sem1.txt").exists(), Path("missing.txt").exists())sem1.txt .txt 17 bytes: Results for sem1 sem2.txt .txt 17 bytes: Results for sem2 True False
Chapter 15 · Real Programs
Exceptions
Handle errors gracefully with try, except, else and finally, read tracebacks, raise your own errors, and define custom exception classes.
When something goes wrong, Python raises an exception. If nothing handles it, the program stops and prints a traceback — read it from the bottom up: the last line names the error.
marks = [80, 90]
print(marks[5])Traceback (most recent call last):
File "main.py", line 2, in <module>
print(marks[5])
~~~~~^^^
IndexError: list index out of rangetry / except
def ask_and_divide():
try:
n = int(input("Divide 100 by: "))
print("Result:", 100 / n)
except ValueError:
print("That was not a whole number.")
except ZeroDivisionError:
print("You cannot divide by zero.")
for _ in range(3):
ask_and_divide()Divide 100 by: ten That was not a whole number. Divide 100 by: 0 You cannot divide by zero. Divide 100 by: 4 Result: 25.0
else and finally
elseruns only if thetryblock raised no exception.finallyruns always — perfect for cleanup.
def read_number(text):
try:
value = float(text)
except ValueError as e: # 'as e' gives the exception object
print(f" error: {e}")
return None
else:
print(" parsed fine")
return value
finally:
print(" (finished trying", repr(text) + ")")
print(read_number("3.5"))
print(read_number("abc"))parsed fine (finished trying '3.5') 3.5 error: could not convert string to float: 'abc' (finished trying 'abc') None
Common built-in exceptions
| Exception | Raised when |
|---|---|
ValueError |
right type, wrong value: int("abc") |
TypeError |
wrong type: "5" + 5 |
ZeroDivisionError |
dividing by zero |
IndexError |
list index out of range |
KeyError |
missing dictionary key |
AttributeError |
missing attribute: None.upper() |
NameError |
using an undefined name |
FileNotFoundError |
opening a file that does not exist |
RecursionError |
recursion too deep |
tests = [lambda: int("x"), lambda: "5" + 5, lambda: [1][3], lambda: {}["k"], lambda: undefined_name]
for t in tests:
try:
t()
except Exception as e: # catch any normal exception
print(f"{type(e).__name__:15} {e}")ValueError invalid literal for int() with base 10: 'x' TypeError can only concatenate str (not "int") to str IndexError list index out of range KeyError 'k' NameError name 'undefined_name' is not defined
raise
Raise an exception yourself when input is invalid:
def set_age(age):
if not isinstance(age, int):
raise TypeError("age must be an integer")
if age < 0 or age > 150:
raise ValueError(f"impossible age: {age}")
return age
for value in (21, -4, "twenty"):
try:
print("ok:", set_age(value))
except (TypeError, ValueError) as e:
print("rejected:", e)ok: 21 rejected: impossible age: -4 rejected: age must be an integer
Custom exceptions
class InsufficientBalance(Exception):
def __init__(self, needed):
super().__init__(f"need Rs {needed} more")
self.needed = needed
class Wallet:
def __init__(self, balance):
self.balance = balance
def pay(self, amount):
if amount > self.balance:
raise InsufficientBalance(amount - self.balance)
self.balance -= amount
w = Wallet(500)
for amount in (200, 450):
try:
w.pay(amount)
print(f"paid {amount}, balance {w.balance}")
except InsufficientBalance as e:
print("payment failed:", e, "| short by", e.needed)paid 200, balance 300 payment failed: need Rs 150 more | short by 150
EAFP vs LBYL
Python style often prefers EAFP — "Easier to Ask Forgiveness than Permission" — try the operation and handle failure, rather than LBYL ("Look Before You Leap") with many if checks:
config = {"theme": "dark"}
# LBYL
size = config["size"] if "size" in config else 12
# EAFP
try:
size = config["size"]
except KeyError:
size = 12
print(size, config.get("size", 12)) # get() is the simplest here12 12
Chapter 16 · Real Programs
Object-Oriented Python
Classes in Python: init and self, instance vs class attributes, methods, properties, inheritance with super(), polymorphism, and dunder methods like str and eq.
Everything in Python is an object — numbers, strings, functions, even classes. You define your own types with class.
class Student:
school = "Kalvium" # class attribute: shared by all students
def __init__(self, name, roll): # the constructor
self.name = name # instance attributes: one per object
self.roll = roll
self.marks = []
def add_mark(self, m): # a method: first parameter is always self
self.marks.append(m)
def average(self):
return sum(self.marks) / len(self.marks) if self.marks else 0
s1 = Student("Asha", 101)
s2 = Student("Ravi", 102)
s1.add_mark(88); s1.add_mark(94)
s2.add_mark(76)
print(s1.name, s1.average(), s1.school)
print(s2.name, s2.average(), Student.school)Asha 91.0 Kalvium Ravi 76.0 Kalvium
self is the object the method was called on — the same idea as this in C++, but written explicitly.
Encapsulation by convention
Python has no private keyword. By convention:
_name— "internal, please don't touch".__name— name-mangled to_ClassName__name, which makes accidental access harder.
Properties let you add validation while keeping attribute syntax:
class Account:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self): # read with acc.balance
return self._balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("deposit must be positive")
self._balance += amount
acc = Account("Priya", 1000)
acc.deposit(500)
print(acc.balance)
try:
acc.balance = 10**6 # no setter defined: read-only
except AttributeError as e:
print("blocked:", e)1500 blocked: property 'balance' of 'Account' object has no setter
Inheritance and super()
class Person:
def __init__(self, name, age):
self.name, self.age = name, age
def introduce(self):
return f"I am {self.name}, {self.age}."
class Student(Person):
def __init__(self, name, age, course):
super().__init__(name, age) # run the parent's constructor
self.course = course
def introduce(self): # override
return super().introduce() + f" I study {self.course}."
class Teacher(Person):
def introduce(self):
return f"Good morning, I am Prof. {self.name}."
people = [Student("Meera", 19, "DSA"), Teacher("Rao", 45), Person("Kiran", 30)]
for p in people:
print(p.introduce()) # polymorphism: each object's own version
print(isinstance(people[0], Person), issubclass(Teacher, Person))I am Meera, 19. I study DSA. Good morning, I am Prof. Rao. I am Kiran, 30. True True
Every method in Python behaves like a C++ virtual function: the call is resolved on the actual object at run time. Python also supports multiple inheritance (class C(A, B)), resolving names with the method resolution order (C.__mro__).
Dunder (magic) methods
Methods with double underscores let your objects work with Python's built-in syntax — this is Python's operator overloading:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __str__(self): # print(v) and str(v)
return f"({self.x}, {self.y})"
def __repr__(self): # developer view, used in lists
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k): # v * 3
return Vector(self.x * k, self.y * k)
def __eq__(self, other): # v1 == v2
return (self.x, self.y) == (other.x, other.y)
def __len__(self): # len(v)
return 2
def __abs__(self): # abs(v): the length
return (self.x ** 2 + self.y ** 2) ** 0.5
a, b = Vector(3, 4), Vector(1, 2)
print(a + b, a * 3, abs(a), a == Vector(3, 4), len(a))
print([a, b])(4, 6) (9, 12) 5.0 True 2 [Vector(3, 4), Vector(1, 2)]
| Method | Enables |
|---|---|
__init__ |
construction: Vector(3, 4) |
__str__, __repr__ |
print(obj), repr(obj) |
__add__, __sub__, __mul__ |
+, -, * |
__eq__, __lt__ |
==, < (and sorting) |
__len__, __getitem__ |
len(obj), obj[i] |
__iter__, __next__ |
for x in obj |
__call__ |
obj() |
Class methods, static methods and dataclasses
from dataclasses import dataclass
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, f): # alternative constructor
return cls((f - 32) * 5 / 9)
@staticmethod
def is_freezing(c): # utility: needs neither self nor cls
return c <= 0
t = Temperature.from_fahrenheit(98.6)
print(round(t.celsius, 1), Temperature.is_freezing(-3))
@dataclass # writes __init__, __repr__ and __eq__ for you
class Book:
title: str
author: str
pages: int = 0
print(Book("Clean Code", "Robert Martin", 464))37.0 True Book(title='Clean Code', author='Robert Martin', pages=464)
Chapter 17 · Real Programs
Iterators & Generators
How for loops really work, the iterator protocol, writing generators with yield, lazy evaluation and infinite sequences, and generator pipelines.
How a for loop works
for x in collection calls iter(collection) to get an iterator, then calls next() on it repeatedly until it raises StopIteration:
nums = [10, 20, 30]
it = iter(nums)
print(next(it), next(it), next(it))
try:
next(it)
except StopIteration:
print("exhausted")10 20 30 exhausted
An iterable is anything you can loop over (lists, strings, dicts, files, ranges). An iterator is the object that actually remembers the position.
Writing an iterator class
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
value = self.current
self.current -= 1
return value
print(list(Countdown(5)))
for n in Countdown(3):
print(n, end=" ")
print("liftoff!")[5, 4, 3, 2, 1, 0] 3 2 1 0 liftoff!
Generators: the easy way
A generator function uses yield instead of return. Each yield hands out one value and pauses the function, keeping all its local variables until the next value is requested:
def countdown(start):
while start >= 0:
yield start
start -= 1
def fibonacci():
a, b = 0, 1
while True: # infinite — fine, values are produced only on demand
yield a
a, b = b, a + b
print(list(countdown(5)))
fib = fibonacci()
print([next(fib) for _ in range(10)])
gen = countdown(2)
print(next(gen), next(gen), next(gen))[5, 4, 3, 2, 1, 0] [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 2 1 0
Why generators matter: lazy evaluation
A generator produces values one at a time, so it uses almost no memory no matter how many values it yields:
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print("list:", sys.getsizeof(squares_list), "bytes")
print("generator:", sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))list: 8448728 bytes generator: 208 bytes 333332833333500000
Generator pipelines
Chain generators like assembly-line stages. Each item flows through all stages before the next item starts:
def read_lines(text):
for line in text.splitlines():
yield line
def non_empty(lines):
for line in lines:
if line.strip():
yield line.strip()
def parse(lines):
for line in lines:
name, score = line.split(",")
yield name, int(score)
data = """Asha,88
Ravi,95
Meera,79
"""
results = parse(non_empty(read_lines(data)))
for name, score in results:
print(f"{name}: {score}")Asha: 88 Ravi: 95 Meera: 79
itertools
import itertools
print(list(itertools.islice(itertools.count(1, 3), 5))) # 1, 4, 7, 10, 13
print(list(itertools.islice(itertools.cycle("AB"), 5)))
print(list(itertools.chain([1, 2], [3], [4, 5])))
print([(k, len(list(g))) for k, g in itertools.groupby("aaabccdddd")])
print(list(itertools.product("xy", [1, 2])))[1, 4, 7, 10, 13]
['A', 'B', 'A', 'B', 'A']
[1, 2, 3, 4, 5]
[('a', 3), ('b', 1), ('c', 2), ('d', 4)]
[('x', 1), ('x', 2), ('y', 1), ('y', 2)]| Iterator class | Generator function | Generator expression | |
|---|---|---|---|
| Syntax | __iter__ + __next__ |
def … yield |
(x for x in xs) |
| Length | most code | short | one line |
| State | kept in attributes | kept automatically | kept automatically |
Chapter 18 · Real Programs
Python Cheat Sheet
Python on one page: syntax, built-in types and their methods, comprehensions, functions, files, exceptions, classes and the standard-library modules you will reach for most.
Syntax at a glance
x = 10 # variables: no declarations
if x > 5: ... # if / elif / else, indentation blocks
for i in range(n): ... # loop over a range or any iterable
while cond: ... # break, continue, loop-else
def f(a, b=2, *args, **kwargs): # functions with defaults and varargs
return a + b
lambda x: x * 2 # anonymous function
class Dog(Animal): # class with inheritance
def __init__(self, name):
super().__init__(name)
try: ... except ValueError as e: ... else: ... finally: ...
with open("f.txt") as f: ... # auto-closing resourcesBuilt-in types
| Type | Literal | Mutable | Ordered | Notes |
|---|---|---|---|---|
int, float, bool |
3, 3.0, True |
no | — | int has unlimited size |
str |
"hi" |
no | yes | slicing, methods, f-strings |
list |
[1, 2] |
yes | yes | dynamic array |
tuple |
(1, 2) |
no | yes | fixed records, dict keys |
set |
{1, 2} |
yes | no | unique items, O(1) in |
dict |
{"k": 1} |
yes | insertion order | key → value, O(1) lookup |
Most-used methods
| Type | Methods |
|---|---|
str |
split, join, strip, replace, find, count, upper, lower, startswith, isdigit |
list |
append, extend, insert, pop, remove, sort, reverse, index, count, copy |
dict |
get, items, keys, values, pop, setdefault, update |
set |
add, discard, remove, union (|), intersection (&), difference (-) |
Built-in functions
len, range, enumerate, zip, sorted, reversed, sum, min, max, abs, round, map, filter, any, all, isinstance, type, input, print, int, float, str, list, dict, set, tuple, open, ord, chr
Comprehensions
[x * x for x in xs if x > 0] # list
{x % 3 for x in xs} # set
{k: v for k, v in pairs} # dict
sum(x for x in xs) # generator expressionSlicing
| Expression | Meaning |
|---|---|
s[a:b] |
from a up to (not including) b |
s[:n] / s[-n:] |
first n / last n items |
s[::-1] |
reversed copy |
s[::2] |
every second item |
Python vs C quick map
| C | Python |
|---|---|
printf("%d\n", x); |
print(x) |
scanf("%d", &x); |
x = int(input()) |
for (i = 0; i < n; i++) |
for i in range(n): |
a && b, a || b, !a |
a and b, a or b, not a |
x++ |
x += 1 |
NULL |
None |
struct |
class / @dataclass |
malloc / free |
automatic memory management |
Programs to practise
- Number programs: prime, factorial, Fibonacci, palindrome, Armstrong, GCD/LCM.
- String programs: reverse words, palindrome sentence, vowel count, anagram check, character frequency.
- List programs: second largest, remove duplicates keeping order, rotate, merge sorted lists.
- Dictionary programs: word frequency, group by first letter, invert a dict, student grade book.
- Recursion: power, sum of digits, flatten nested lists, permutations.
- Files: count lines/words/characters, CSV average, JSON save/load.
- OOP: BankAccount with validation, Shape hierarchy, Vector with dunder methods.
- Generators: Fibonacci generator, reading a large file lazily.