Code Journey
Python

Python.

Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python’s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many areas on most platforms.

Common Use Cases

  • Web Development – Python is used to build websites and web applications using frameworks like Django and Flask.
  • AI/ML – Python is the go-to language for artificial intelligence and machine learning using libraries like TensorFlow, PyTorch, and scikit-learn.
  • Data Science & Mathematics – Python is widely used for data analysis and mathematical computation through tools like NumPy, Pandas, and Matplotlib.
  • Software Development – Python is used to write scripts, automate tasks, and build a wide range of applications.
  • Cybersecurity & Networking – Python is commonly used to develop security tools and network scripts.

Variables & Data Types

3 Topics

Declaring Variables

x = 10           # int
y = 3.14         # float
name = "Alice"   # str
is_active = True  # bool
nothing = None   # NoneType

# Python is dynamically typed
# No need to declare type explicitly

Type Checking & Casting

x = 42
type(x)          # <class 'int'>
type("hello")    # <class 'str'>
type(3.14)       # <class 'float'>

# Type Casting
str(10)          # '10'
int('5')         # 5
int(3.9)         # 3  (truncates, not rounds)
float('3.14')    # 3.14
bool(0)          # False
bool("")         # False
bool("hello")    # True
bool(1)          # True

Multiple Assignment & Swap

# Assign multiple variables at once
a, b, c = 1, 2, 3

# Assign same value to multiple variables
x = y = z = 0

# Swap values without a temp variable
a, b = b, a

# Unpack a list into variables
coords = [10, 20, 30]
x, y, z = coords

# Ignore values with _
first, _, last = ["Alice", "ignored", "Smith"]

Strings

3 Topics

String Methods

s = "  Hello, World!  "
s.strip()           # 'Hello, World!' (removes spaces)
s.upper()           # '  HELLO, WORLD!  '
s.lower()           # '  hello, world!  '
s.replace('o','0')  # '  Hell0, W0rld!  '
s.split(',')        # ['  Hello', ' World!  ']
','.join(['a','b','c'])  # 'a,b,c'
s.startswith('  H') # True
s.endswith('!  ')   # True
s.count('l')        # 3
s.find('World')     # 9 (index) or -1 if not found

Indexing & Slicing

s = "Python"

# Indexing
s[0]        # 'P'  (first char)
s[-1]       # 'n'  (last char)
s[-2]       # 'o'  (second to last)

# Slicing [start:end:step]
s[0:3]      # 'Pyt' (index 0,1,2)
s[2:]       # 'thon' (from index 2 to end)
s[:3]       # 'Pyt' (from start to index 2)
s[::-1]     # 'nohtyP' (reversed)
s[::2]      # 'Pto' (every 2nd character)

f-Strings (String Formatting)

name = "Alice"
age = 30
pi = 3.14159

# Basic f-string
print(f"Name: {name}, Age: {age}")

# Expressions inside f-strings
print(f"Next year: {age + 1}")
print(f"Is adult: {age >= 18}")

# Number formatting
print(f"{pi:.2f}")       # '3.14' (2 decimal places)
print(f"{10000:,}")      # '10,000' (comma separator)
print(f"{0.85:.0%}")     # '85%' (percentage)

# Padding & alignment
print(f"{name:>10}")     # '     Alice' (right align)
print(f"{name:<10}")     # 'Alice     ' (left align)
print(f"{name:^10}")     # '  Alice   ' (center)

Lists

2 Topics

List Operations

fruits = ["apple", "banana", "cherry"]

# Access
fruits[0]           # 'apple'
fruits[-1]          # 'cherry'
len(fruits)         # 3

# Modify
fruits.append("mango")       # add to end
fruits.insert(1, "grape")    # insert at index 1
fruits.extend(["kiwi","pear"]) # add multiple
fruits.remove("banana")      # remove by value
fruits.pop()                 # remove & return last
fruits.pop(0)                # remove & return at index

# Organize
fruits.sort()                # sort in place
fruits.sort(reverse=True)    # descending
fruits.reverse()             # reverse in place
sorted(fruits)               # returns new sorted list

List Comprehension

# Basic: [expression for item in iterable]
squares = [x**2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# With condition: [expr for item in iter if condition]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Transform strings
words = ["hello", "world"]
upper = [w.upper() for w in words]
# ['HELLO', 'WORLD']

# Nested (flatten a 2D list)
matrix = [[1,2],[3,4],[5,6]]
flat = [n for row in matrix for n in row]
# [1, 2, 3, 4, 5, 6]

Tuples & Sets

2 Topics

Tuples

# Creating tuples
coords = (10, 20)
point = 1, 2, 3     # parentheses optional
single = (42,)       # comma required for single item!

# Access (same as list)
coords[0]            # 10
coords[-1]           # 20

# Unpacking
x, y = coords
first, *rest = (1, 2, 3, 4)  # first=1, rest=[2,3,4]

# Tuple methods
t = (1, 2, 2, 3)
t.count(2)           # 2 (occurrences)
t.index(3)           # 3 (first index of value)

# Tuples are immutable
# coords[0] = 99     # TypeError!

Sets

# Creating sets
s = {1, 2, 3, 3, 2}   # {1, 2, 3} — auto deduplication
empty = set()          # NOT {} — that creates a dict!

# Modifying
s.add(4)               # add one item
s.update([5, 6])       # add multiple items
s.remove(1)            # removes; raises KeyError if missing
s.discard(99)          # removes; no error if missing

# Set operations
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a & b    # {3, 4}         — intersection
a | b    # {1,2,3,4,5,6}  — union
a - b    # {1, 2}         — difference (in a, not b)
a ^ b    # {1,2,5,6}      — symmetric difference

Dictionaries

2 Topics

Dictionary Operations

person = {"name": "Alice", "age": 30, "city": "NYC"}

# Access
person["name"]          # 'Alice'
person.get("age")       # 30
person.get("email", "N/A")  # 'N/A' (default if missing)

# Modify
person["email"] = "a@b.com"  # add or update
del person["city"]           # delete a key
removed = person.pop("age")  # delete + return value

# Iterate
for key in person:
    print(key)
for key, val in person.items():
    print(f"{key}: {val}")

# Check membership
"name" in person        # True
"salary" not in person  # True

# Useful methods
list(person.keys())     # list of keys
list(person.values())   # list of values
person.update({"age": 31, "city": "LA"})  # merge

Dict Comprehension & Merging

# Dict comprehension: {key: value for item in iterable}
squares = {x: x**2 for x in range(6)}
# {0:0, 1:1, 2:4, 3:9, 4:16, 5:25}

# With condition
even_sq = {x: x**2 for x in range(10) if x % 2 == 0}

# Invert a dict (swap keys and values)
orig = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in orig.items()}
# {1: 'a', 2: 'b', 3: 'c'}

# Merging dicts
d1 = {"a": 1, "b": 2}
d2 = {"b": 99, "c": 3}

# Python 3.9+ (| operator)
merged = d1 | d2       # {'a':1, 'b':99, 'c':3}

# Python 3.5+ (** unpacking)
merged = {**d1, **d2}  # same result

Control Flow

3 Topics

if / elif / else

score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

# Ternary (one-line conditional)
label = "Pass" if score >= 50 else "Fail"

# Chained comparisons (unique to Python)
age = 25
if 18 <= age <= 65:
    print("Working age")

# Truthy / Falsy shorthand
name = ""
if not name:
    print("Name is empty")

for & while Loops

# for loop over range
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10, 2): # 2, 4, 6, 8 (start, stop, step)
    print(i)

# for loop over a list
fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

# enumerate: index + value
for i, fruit in enumerate(fruits, start=1):
    print(f"{i}. {fruit}")

# zip: loop two lists together
names = ["Alice", "Bob"]
scores = [95, 87]
for name, score in zip(names, scores):
    print(f"{name}: {score}")

# while loop
count = 0
while count < 5:
    print(count)
    count += 1

break, continue & pass

# break: exit loop entirely
for i in range(10):
    if i == 5:
        break
    print(i)  # prints 0, 1, 2, 3, 4

# continue: skip current iteration
for i in range(10):
    if i % 2 == 0:
        continue
    print(i)  # prints 1, 3, 5, 7, 9

# for...else: else runs if loop wasn't broken
for i in range(5):
    if i == 10:
        break
else:
    print("Loop completed without break")

# pass: do nothing (placeholder)
def todo():
    pass  # implement later

class Empty:
    pass  # empty class placeholder

Functions

3 Topics

Defining Functions

# Basic function
def greet(name):
    return f"Hello, {name}!"

# Default parameter values
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

greet("Alice")             # 'Hello, Alice!'
greet("Bob", "Hi")        # 'Hi, Bob!'
greet(greeting="Hey", name="Eve")  # keyword args

# Multiple return values (as tuple)
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 1, 4, 1, 5, 9])
# low=1, high=9

*args and **kwargs

# *args: accepts any number of positional arguments
def total(*args):
    print(args)    # it's a tuple
    return sum(args)

total(1, 2, 3)        # 6
total(10, 20, 30, 40) # 100

# **kwargs: accepts any number of keyword arguments
def print_info(**kwargs):
    print(kwargs)  # it's a dict
    for k, v in kwargs.items():
        print(f"{k}: {v}")

print_info(name="Alice", age=30, city="NYC")

# Combining all types (order matters!)
def func(pos, /, normal, *, keyword_only):
    pass

# Unpacking when calling
nums = [1, 2, 3]
print(*nums)           # same as print(1, 2, 3)

data = {"name": "Alice", "age": 30}
greet(**data)          # same as greet(name="Alice", age=30)

Lambda Functions

# lambda arguments: expression
square = lambda x: x ** 2
add = lambda x, y: x + y

square(5)   # 25
add(3, 4)   # 7

# Common use: sorting by custom key
people = [("Alice", 30), ("Bob", 25), ("Eve", 35)]
people.sort(key=lambda p: p[1])  # sort by age
# [("Bob", 25), ("Alice", 30), ("Eve", 35)]

# map() + lambda: apply function to all items
nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, nums))
# [1, 4, 9, 16, 25]

# filter() + lambda: keep items matching condition
evens = list(filter(lambda x: x % 2 == 0, nums))
# [2, 4]

Object-Oriented Programming (OOP)

3 Topics

Classes & Objects

class Dog:
    # Class variable (shared by all instances)
    species = "Canis lupus familiaris"

    # Constructor
    def __init__(self, name, age):
        self.name = name   # instance variable
        self.age = age

    # Instance method
    def bark(self):
        return f"{self.name} says: Woof!"

    def describe(self):
        return f"{self.name} is {self.age} years old"

    # String representation
    def __repr__(self):
        return f"Dog(name={self.name!r}, age={self.age})"

# Create instances
rex = Dog("Rex", 3)
bella = Dog("Bella", 5)

rex.bark()        # 'Rex says: Woof!'
rex.describe()    # 'Rex is 3 years old'
Dog.species       # 'Canis lupus familiaris'
print(rex)        # Dog(name='Rex', age=3)

Inheritance & super()

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

    def __repr__(self):
        return f"{type(self).__name__}({self.name!r})"

class Dog(Animal):
    def speak(self):  # override parent method
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):  # override parent method
        return f"{self.name} says Meow!"

class GuideDog(Dog):
    def __init__(self, name, owner):
        super().__init__(name)  # call parent __init__
        self.owner = owner

    def guide(self):
        return f"{self.name} guides {self.owner}"

# Polymorphism
animals = [Dog("Rex"), Cat("Whiskers"), GuideDog("Buddy", "Alice")]
for a in animals:
    print(a.speak())  # calls each class's own speak()

Dunder / Magic Methods

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other):          # v1 + v2
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):          # v1 - v2
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):         # v * 3
        return Vector(self.x * scalar, self.y * scalar)

    def __len__(self):                 # len(v)
        return 2

    def __eq__(self, other):           # v1 == v2
        return self.x == other.x and self.y == other.y

v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2    # Vector(4, 6)
v1 * 3     # Vector(3, 6)
v1 == v2   # False

Error Handling

2 Topics

try / except / finally

# Basic try-except
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")

# Catching multiple exceptions
try:
    val = int("abc")
except (ValueError, TypeError) as e:
    print(f"Error: {e}")

# Full structure
try:
    f = open("data.txt")
    data = f.read()
except FileNotFoundError as e:
    print(f"File not found: {e}")
except PermissionError:
    print("Permission denied")
except Exception as e:       # catches any other error
    print(f"Unexpected: {e}")
else:
    print("File read successfully!")  # no error occurred
finally:
    print("Done.")  # ALWAYS runs

Raising & Custom Exceptions

# Raise a built-in exception
def divide(a, b):
    if b == 0:
        raise ValueError("Denominator cannot be zero")
    return a / b

# Re-raise an exception
try:
    divide(10, 0)
except ValueError as e:
    print(f"Caught: {e}")
    raise  # re-raises the same exception

# Custom exception classes
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Need {amount}, have {balance}")

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    withdraw(100, 200)
except InsufficientFundsError as e:
    print(e)  # 'Need 200, have 100'

File I/O

2 Topics

Reading & Writing Files

# Write (creates file or overwrites)
with open("data.txt", "w") as f:
    f.write("Hello, World!\n")
    f.write("Second line\n")

# Append (adds to end without overwriting)
with open("data.txt", "a") as f:
    f.write("Third line\n")

# Read entire file as one string
with open("data.txt", "r") as f:
    content = f.read()

# Read into a list of lines
with open("data.txt") as f:
    lines = f.readlines()  # includes \n

# Read line by line (memory efficient for large files)
with open("data.txt") as f:
    for line in f:
        print(line.strip())  # strip() removes \n

# File open modes
# 'r'  — read (default)
# 'w'  — write (overwrites)
# 'a'  — append
# 'rb' — read binary (images, PDFs)
# 'x'  — create (fails if file exists)

Working with JSON & CSV

import json
import csv

# JSON write
data = {"name": "Alice", "scores": [95, 87, 92]}
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

# JSON read
with open("data.json") as f:
    loaded = json.load(f)
print(loaded["name"])  # 'Alice'

# JSON to/from string
json_str = json.dumps(data)
back = json.loads(json_str)

# CSV write
rows = [["Alice", 30], ["Bob", 25]]
with open("people.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Name", "Age"])  # header
    writer.writerows(rows)

# CSV read
with open("people.csv") as f:
    reader = csv.DictReader(f)  # header as keys
    for row in reader:
        print(row["Name"], row["Age"])

Modules & Imports

2 Topics

Importing Modules

# Import full module
import math
math.sqrt(16)     # 4.0
math.pi           # 3.14159265...
math.ceil(4.2)    # 5
math.floor(4.9)   # 4

# Import specific names
from math import sqrt, pi
sqrt(25)          # 5.0

# Import with alias
import numpy as np   # common convention
import pandas as pd

# Import everything (avoid in production)
from math import *

# Standard library examples
import os
os.getcwd()           # current directory
os.listdir('.')        # list files
os.path.exists('f.txt')  # check file exists

import sys
sys.version           # Python version
sys.argv              # command-line arguments

import random
random.randint(1, 10)    # random int
random.choice([1,2,3])   # random item
random.shuffle([1,2,3])  # shuffle in place

pip & Virtual Environments

# pip — Python's package manager
pip install requests           # install package
pip install numpy pandas        # install multiple
pip install 'django>=4.0'       # specific version
pip uninstall package-name      # remove
pip list                        # show installed
pip show requests               # package details
pip freeze > requirements.txt   # export all deps
pip install -r requirements.txt # install from file

# Virtual environments (isolate dependencies per project)
python -m venv venv             # create environment
source venv/bin/activate        # activate (Mac/Linux)
venv\Scripts\activate           # activate (Windows)
pip install ...                 # installs to venv only
deactivate                      # exit venv

# .gitignore: always exclude venv/
# venv/

Comprehensions & Generators

1 Topics

All Comprehension Types

# List comprehension
squares = [x**2 for x in range(6)]
# [0, 1, 4, 9, 16, 25]

# Dict comprehension
word_len = {w: len(w) for w in ["hi", "hello", "hey"]}
# {'hi': 2, 'hello': 5, 'hey': 3}

# Set comprehension (auto-deduplicates)
unique_mod = {x % 3 for x in range(10)}
# {0, 1, 2}

# Generator expression (lazy — values computed on demand)
gen = (x**2 for x in range(1000000))  # no memory used yet!
next(gen)   # 0
next(gen)   # 1

# Generator function with yield
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for num in countdown(5):
    print(num)  # 5, 4, 3, 2, 1

Decorators

1 Topics

Creating & Using Decorators

import time

# A decorator is a function that wraps another function
def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)  # call original
        end = time.time()
        print(f"{func.__name__} took {end-start:.4f}s")
        return result
    return wrapper

# Apply decorator with @ syntax
@timer
def slow_function():
    time.sleep(1)
    return "done"

slow_function()  # prints: slow_function took 1.0001s

# functools.wraps preserves the original function's metadata
from functools import wraps

def logger(func):
    @wraps(func)  # preserves __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Context Managers

1 Topics

with Statement & Custom Context Managers

# Built-in: file handling
with open("file.txt") as f:
    data = f.read()  # file auto-closed after block

# Multiple context managers
with open("input.txt") as fin, open("output.txt", "w") as fout:
    fout.write(fin.read())

# Creating a custom context manager with contextlib
from contextlib import contextmanager

@contextmanager
def managed_resource(name):
    print(f"Acquiring {name}")
    try:
        yield name  # 'as' receives this value
    finally:
        print(f"Releasing {name}")

with managed_resource("DB connection") as res:
    print(f"Using {res}")
# Output:
# Acquiring DB connection
# Using DB connection
# Releasing DB connection

Useful Built-in Functions

2 Topics

Iteration Built-ins

# map(): apply function to all items
list(map(str, [1, 2, 3]))        # ['1','2','3']
list(map(lambda x: x*2, [1,2,3])) # [2, 4, 6]

# filter(): keep items matching condition
list(filter(lambda x: x > 2, [1,2,3,4]))  # [3, 4]

# zip(): pair items from multiple iterables
list(zip([1,2,3], ['a','b','c']))
# [(1,'a'), (2,'b'), (3,'c')]

# enumerate(): index + value
for i, val in enumerate(['a','b','c'], start=1):
    print(i, val)  # 1 a, 2 b, 3 c

# sorted() with key
people = ["Alice", "Bob", "Charlie"]
sorted(people, key=len)  # ['Bob','Alice','Charlie']
sorted(people, key=str.lower, reverse=True)

# any() and all()
any([False, True, False])   # True
all([True, True, False])    # False
any(x > 5 for x in [1,3,7]) # True (generator!)

Math & Type Built-ins

# Math
abs(-7)          # 7
round(3.567, 2)  # 3.57 (2 decimal places)
round(3.5)       # 4 (rounds to even in Python 3!)
pow(2, 10)       # 1024 (same as 2**10)
divmod(17, 5)    # (3, 2) → (quotient, remainder)
max([3,1,4,1,5]) # 5
min([3,1,4,1,5]) # 1
sum([1,2,3,4])   # 10

# Type checking
type(42) == int         # True
isinstance(42, int)     # True (preferred over type())
isinstance(42, (int, float))  # True (checks either)

# Conversion
bin(10)     # '0b1010' (binary string)
oct(8)      # '0o10'   (octal string)
hex(255)    # '0xff'   (hex string)
ord('A')    # 65       (char to ASCII code)
chr(65)     # 'A'      (ASCII code to char)

itertools & functools

2 Topics

itertools — Efficient Iteration

import itertools

# chain: combine multiple iterables
list(itertools.chain([1,2], [3,4], [5]))
# [1, 2, 3, 4, 5]

# product: cartesian product
list(itertools.product([1,2], ['a','b']))
# [(1,'a'),(1,'b'),(2,'a'),(2,'b')]

# permutations & combinations
list(itertools.permutations('ABC', 2))
# [('A','B'),('A','C'),('B','A'),...]
list(itertools.combinations('ABC', 2))
# [('A','B'),('A','C'),('B','C')]

# groupby: group consecutive items
data = [("A",1),("A",2),("B",3),("B",4)]
for key, grp in itertools.groupby(data, key=lambda x: x[0]):
    print(key, list(grp))
# A [('A',1),('A',2)]
# B [('B',3),('B',4)]

# islice: slice a generator
list(itertools.islice(range(100), 5))  # [0,1,2,3,4]

functools — Higher-Order Functions

from functools import reduce, lru_cache, partial

# reduce: apply function cumulatively
reduce(lambda x, y: x * y, [1,2,3,4,5])  # 120
reduce(lambda x, y: x + y, [1,2,3])      # 6

# lru_cache: memoize expensive function results
@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

fibonacci(50)  # instant — results cached!
fibonacci.cache_info()  # CacheInfo(hits=..., misses=...)

# partial: freeze some arguments of a function
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)
cube   = partial(power, exponent=3)

square(5)  # 25
cube(3)    # 27

Type Hints

1 Topics

Type Hints & Annotations

from typing import Optional, Union, List, Dict, Tuple

# Function with type hints
def greet(name: str, times: int = 1) -> str:
    return ("Hello, " + name + "! ") * times

# Variable annotations
age: int = 30
name: str = "Alice"
scores: list[int] = [90, 85, 92]  # Python 3.9+

# Optional (can be None)
def find_user(id: int) -> Optional[str]:
    return None  # or a string

# Union (multiple possible types)
def process(val: Union[int, str]) -> str:
    return str(val)

# Python 3.10+ shorthand
def process(val: int | str) -> str:
    return str(val)

# Dict and Tuple hints
def stats(data: list[float]) -> dict[str, float]:
    return {"min": min(data), "max": max(data)}

def coords() -> tuple[int, int]:
    return (10, 20)

Dataclasses

1 Topics

Using @dataclass

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)          # Point(x=1.0, y=2.0)
p.x               # 1.0
p == Point(1,2)   # True (auto __eq__)

# With defaults and post-init
@dataclass
class Student:
    name: str
    age: int
    grades: list = field(default_factory=list)
    school: str = "MIT"

    def average(self) -> float:
        return sum(self.grades) / len(self.grades) if self.grades else 0

s = Student("Alice", 20, [90, 85, 95])
print(s)          # Student(name='Alice', age=20, ...)
s.average()       # 90.0

# Frozen (immutable) dataclass
@dataclass(frozen=True)
class Config:
    host: str
    port: int = 8080

cfg = Config("localhost")
# cfg.host = "x"  # FrozenInstanceError!

Concurrency Basics

2 Topics

Threading & Multiprocessing

import threading
import multiprocessing

# Threading (good for I/O-bound tasks)
def download(url):
    print(f"Downloading {url}")

threads = [
    threading.Thread(target=download, args=(url,))
    for url in ["url1", "url2", "url3"]
]
for t in threads: t.start()
for t in threads: t.join()  # wait for all

# Multiprocessing (good for CPU-bound tasks)
def compute(n):
    return sum(range(n))

with multiprocessing.Pool(4) as pool:
    results = pool.map(compute, [10**6]*4)
print(results)

asyncio — Async/Await

import asyncio

# Define async function (coroutine)
async def fetch_data(name, delay):
    print(f"Fetching {name}...")
    await asyncio.sleep(delay)  # non-blocking wait
    print(f"{name} done!")
    return f"data from {name}"

# Run multiple coroutines concurrently
async def main():
    results = await asyncio.gather(
        fetch_data("API-1", 2),
        fetch_data("API-2", 1),
        fetch_data("API-3", 3),
    )
    print(results)

# Run the event loop
asyncio.run(main())
# Fetching API-1, API-2, API-3... (all start immediately)
# API-2 done! (after 1s)
# API-1 done! (after 2s)
# API-3 done! (after 3s)
# Total: ~3s (not 6s!)