Bytebook Compiler

Bytebook · PYTHON

Python Programming

Python from first print() to real programs: data types, control flow, lists, dictionaries, functions, files, exceptions, OOP and generators.

Back to lessons

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.

Python
print("Hello, Python!")
print("2 + 3 =", 2 + 3)
print("Python" * 3)
Output
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
Terminal
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:

Python shell
>>> 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.

Python
marks = 82
if marks >= 40:
    print("Pass")          # inside the if block
    print("Well done!")
print("Result printed")    # back outside the block
Output
Pass
Well done!
Result printed

Comments

Python
# 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

Python
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")
Output
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.

Python
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))
Output
<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:

Python
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)
Output
1267650600228229401496703205376
302 digits in 2^1000
25! = 15511210043330985984000000

Converting between types

Python
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))
Output
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_case for variables and functions, PascalCase for classes, UPPER_CASE for constants.

Multiple assignment and swapping

Python
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)
Output
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.

Python
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)
Output
[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
Python
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))
Output
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:

Python
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 first
Output
True
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:

Python
name = ""
print(name or "Guest")        # empty string is falsy, so "Guest"
print(0 and 10, 5 and 10)
Output
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?
Python
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 None
Output
True 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:

Python
name = input("Your name: ")
age = int(input("Your age: "))
print(f"Hi {name}, next year you will be {age + 1}.")
Output
Your name: Ravi
Your age: 19
Hi Ravi, next year you will be 20.
Sample input is includedTry it Yourself

Read several numbers from one line with split() and map():

Python
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))
Output
Three numbers: 12 7 30
sum = 49 max = 30
[4, 8, 15, 16, 23, 42] average 18.0
Sample input is includedTry it Yourself

Formatted output

Python
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 style
Output
Notebook       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.

Python
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 character
Output
7 K m l
Kal vium Kalv ium
muivlaK
Klim
K0a1l2v3i4u5m6s[0]s[-1]
Indexes 0 to 6 from the left; −1 to −7 from the right. s[1:4] is "alv".

Strings are immutable

You cannot change a character in place; build a new string instead:

Python
s = "python"
# s[0] = "P"           # TypeError: 'str' object does not support item assignment
s = "P" + s[1:]
print(s)
Output
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
Python
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"]))
Output
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

Python
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))
Output
10 4 2 2
0 a 97
1 b 98
2 c 99
A ₹

Classic string programs

Python
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"))
Output
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.

Python
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)
Output
Marks: 73
Grade: A
Sample input is includedTry it Yourself

Key points:

  • A colon : ends the condition line; the block is the indented lines below.
  • elif (else-if) can repeat; else is optional and comes last.
  • Conditions can be any value — Python uses its truthiness.

Nested conditions and combining tests

Python
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")
Output
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 ?::

Python
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 ifs
Output
7 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.

Python
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))
Output
'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:

Python
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"))
Output
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 …

Python
for subject in ["C", "DSA", "Python"]:
    print("Studying", subject)

for ch in "abc":
    print(ch.upper(), end=" ")
print()
Output
Studying C
Studying DSA
Studying Python
A B C

range()

range(start, stop, step) produces numbers from start up to but not including stop:

Python
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}")
Output
[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

Python
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}")
Output
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:

Python
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}")
Output
Number (0 to stop): 5
Number (0 to stop): 12
Number (0 to stop): -3
Number (0 to stop): 0
3 numbers, sum 14
Sample input is includedTry it Yourself

break, continue and loop-else

  • break leaves the loop immediately.
  • continue skips to the next iteration.
  • A loop's else block runs only if the loop finished without break — perfect for searches.
Python
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()
Output
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
1 2 4 5 7 8 10

Classic loop programs

Python
# 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)
Output
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:

Python
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)))
Output
* 
* * 
* * * 
* * * * 
* * * * * 

    *
   ***
  *****
 *******
*********

1
1 2
1 2 3
1 2 3 4
1 2 3 4 5

Chapter 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.

Python
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 list
Output
78 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
Python
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"))
Output
['pen', 'pencil', 'notebook', 'eraser', 'ruler', 'glue']
popped ruler -> ['pen', 'pencil', 'notebook', 'eraser']
['pencil', 'notebook', 'eraser'] True 2

Sorting and searching

Python
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)
Output
[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

Python
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 independent
Output
a [100, 2, [3, 4, 5]]
b [100, 2, [3, 4, 5]]
c [1, 2, [3, 4, 5]]
d [1, 2, [3, 4]]

2D lists

Python
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)
Output
[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

Python
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 ends
Output
3 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.

Python
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 immutable
Output
3 (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
Python
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)
Output
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.

Python
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
Output
{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
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)
Output
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.

Python
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)
Output
Asha
{'name': 'Asha', 'cgpa': 9.3, 'city': 'Pune'}
3 True False

Safe access

d[key] raises KeyError if the key is missing. get returns a default instead:

Python
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)
Output
10 None 0
25 {'pen': 10, 'notebook': 45, 'ruler': 25}
10 {'notebook': 45, 'ruler': 25}

Looping

Python
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))
Output
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

Python
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())})
Output
3 1
[('the', 3), ('quick', 1)]
{3: ['dog', 'end', 'fox', 'the'], 4: ['lazy', 'over'], 5: ['brown', 'jumps', 'quick']}

Nested dictionaries

Python
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")
Output
CSE101: Programming in C (4 credits) - 2 students
CSE102: Data Structures (4 credits) - 2 students

Building and merging

Python
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])
Output
{'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:

Python
[expression for item in iterable if condition]
Python
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)
Output
[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:

Python
evens = []
for n in nums:
    if n % 2 == 0:
        evens.append(n)

Set and dictionary comprehensions

Python
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)
Output
['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:

Python
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)
Output
[(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:

Python
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"]))
Output
338350
True
False
(6, 'banana')

Real examples

Python
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)
Output
['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.

Python
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__)
Output
Hello, Asha!
21 20
Return a friendly greeting.

A function without a return statement returns None.

Arguments: positional, keyword and default

Python
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 keywords
Output
750.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.

Python
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 arguments
Output
18.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.

Python
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)
Output
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.

Python
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)
Output
[1, 2, 99]

Functions are objects

Functions can be stored in variables, passed to other functions and returned from them:

Python
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))
Output
20 21
42

Type hints

Optional annotations document expected types (Python does not enforce them, but editors and checkers do):

Python
def bmi(weight_kg: float, height_m: float) -> float:
    return round(weight_kg / height_m ** 2, 1)

print(bmi(72.5, 1.68))
Output
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.

Python
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])
Output
81 7
[('Ravi', 95), ('Asha', 88), ('Meera', 79)]
Ravi

Use 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
Python
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"])))
Output
[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.

Python
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"))
Output
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):

Python
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))
Output
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:

Python
import sys
print(sys.getrecursionlimit())

def countdown(n):
    return 0 if n == 0 else countdown(n - 1)

print(countdown(500))
print(countdown(5000))
Output
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 exceeded

For 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.

Pythongeometry.py
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")
Python
import geometry
from geometry import rectangle_area
import geometry as geo

print(geometry.circle_area(2))
print(rectangle_area(3, 4))
print(geo.PI)
Output
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:

Terminal
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 environment

Tour of the standard library

Python
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"))
Output · yours may differ
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
Python
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)
Output
[('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:

Python
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"))
Output
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

Python
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")
Output
'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:

Python
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}")
Output
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:

Python
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
Output
['C', 'Python'] True
{"ok": true, "count": 3}

Paths with pathlib

Python
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())
Output
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.

Python
marks = [80, 90]
print(marks[5])
Output
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    print(marks[5])
          ~~~~~^^^
IndexError: list index out of range

try / except

Python
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()
Output
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
Sample input is includedTry it Yourself

else and finally

  • else runs only if the try block raised no exception.
  • finally runs always — perfect for cleanup.
Python
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"))
Output
  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
Python
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}")
Output
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:

Python
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)
Output
ok: 21
rejected: impossible age: -4
rejected: age must be an integer

Custom exceptions

Python
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)
Output
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:

Python
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 here
Output
12 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.

Python
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)
Output
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:

Python
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)
Output
1500
blocked: property 'balance' of 'Account' object has no setter

Inheritance and super()

Python
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))
Output
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:

Python
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])
Output
(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

Python
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))
Output
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:

Python
nums = [10, 20, 30]
it = iter(nums)
print(next(it), next(it), next(it))
try:
    next(it)
except StopIteration:
    print("exhausted")
Output
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

Python
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!")
Output
[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:

Python
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))
Output
[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:

Python
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))
Output · yours may differ
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:

Python
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}")
Output
Asha: 88
Ravi: 95
Meera: 79

itertools

Python
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])))
Output
[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

Python
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 resources

Built-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

Python
[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 expression

Slicing

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

  1. Number programs: prime, factorial, Fibonacci, palindrome, Armstrong, GCD/LCM.
  2. String programs: reverse words, palindrome sentence, vowel count, anagram check, character frequency.
  3. List programs: second largest, remove duplicates keeping order, rotate, merge sorted lists.
  4. Dictionary programs: word frequency, group by first letter, invert a dict, student grade book.
  5. Recursion: power, sum of digits, flatten nested lists, permutations.
  6. Files: count lines/words/characters, CSV average, JSON save/load.
  7. OOP: BankAccount with validation, Shape hierarchy, Vector with dunder methods.
  8. Generators: Fibonacci generator, reading a large file lazily.