Python *args and **kwargs

  1. In Python, functions can accept a variable number of arguments using two special syntax forms:


  2. Positional Variable Arguments: *args

  3. def greet(*args):
        print(args)
    
    greet("Hello", "World", 42)
    
    ('Hello', 'World', 42)
    

    def add_all(*args):
        return sum(args)
    
    print(add_all(1, 2, 3, 4))
    


  4. Keyword Variable Arguments: **kwargs

  5. def show_info(**kwargs):
        print(kwargs)
    
    show_info(name="Ammy", age=25, city="Berlin")
    
    {'name': 'Ammy', 'age': 25, 'city': 'Berlin'}

    def print_info(**kwargs):
        for key, value in kwargs.items():
            print(key, "=", value)
    


  6. Combining Positional and Keyword Arguments

  7. def func(a, b, *args, **kwargs):
        print("a =", a)
        print("b =", b)
        print("args =", args)
        print("kwargs =", kwargs)
    
    func(1, 2, 3, 4, x=10, y=20)
    
    
    a = 1
    b = 2
    args = (3, 4)
    kwargs = {'x': 10, 'y': 20}
    

    1. normal positional parameters
    2. *args
    3. keyword-only parameters (optional)
    4. **kwargs
    
    def example(a, *args, sep="-", **kwargs):
        pass
    


  8. Argument Unpacking Using * and **

  9. def add(a, b, c):
        return a + b + c
    
    nums = [1, 2, 3]
    print(add(*nums))
    
    def format_user(name, age, city):
        return f"{name}, {age}, from {city}"
    
    info = {"name": "Bob", "age": 30, "city": "Hamburg"}
    print(format_user(**info))
    


  10. Real Use Case: Wrapping Functions (Forwarding Arguments)

  11. def debug(func):
        def wrapper(*args, **kwargs):
            print("Calling:", func.__name__)
            return func(*args, **kwargs)  # forward everything
        return wrapper
    
    @debug
    def multiply(x, y):
        return x * y
    
    print(multiply(3, 4))
    



  12. When to Use *args and **kwargs?

  13. Use Case *args **kwargs
    Unknown number of positional arguments Yes No
    Unknown number of keyword arguments No Yes
    Forwarding arguments in decorators/wrappers Yes Yes
    Passing configuration options No Yes
    Building flexible API helpers Yes Yes


  14. Common Mistakes

  15. def wrong(**kwargs, *args):  # ❌ invalid

    def f(args):  # ❌ not the same as *args
        pass
    

    def f(a, b):
        return a + b
    
    nums = (1, 2)
    # f(nums) ❌ TypeError
    f(*nums)   # ✔ correct
    


  16. Summary

  17. Concept Description
    *args Captures extra positional arguments into a tuple.
    **kwargs Captures extra keyword arguments into a dictionary.
    Unpacking * unpacks lists/tuples, ** unpacks dictionaries.
    Decorator usage Forward arguments to wrapped functions easily.
    Best practice a, *args, sep="-", **kwargs follows correct ordering.




Python match Statement (Structural Pattern Matching)

  1. Introduction



  2. Basic Value Matching

  3. def check_status(code):
        match code:
            case 200:
                return "OK"
            case 404:
                return "Not Found"
            case 500:
                return "Server Error"
            case _:
                return "Unknown"
    



  4. Match Multiple Values in One Case

  5. match command:
        case "start" | "run":
            print("Starting...")
        case "stop" | "quit":
            print("Stopping...")
        case _:
            print("Unknown command")
    



  6. Capturing Values

  7. # Example commands your program might receive
    user_input = ("add", 10, 20)
    # user_input = ("echo", "Hello")
    # user_input = ("quit",)
    
    match user_input:
        case ("add", x, y):
            # The tuple matches ("add", something, something)
            # The two values are captured as x and y
            print(x + y)
    
        case ("echo", message):
            # Matches a tuple with 2 elements: ("echo", some_text)
            # 'message' captures the second element
            print(message)
    
        case ("quit",):
            # Matches a single-element tuple
            print("Goodbye!")
    
        case _:
            # Anything else that doesn't fit the patterns
            print("Unknown command")
    



  8. Sequence Pattern Matching

  9. match data:
        case [x, y]:
            print(f"Two elements: {x}, {y}")
        case [x, y, z]:
            print(f"Three elements: {x}, {y}, {z}")
        case [first, *rest]:
            print("First element:", first)
            print("Remaining:", rest)
    



  10. Matching Dictionaries

  11. match config:
        case {"mode": "debug", "level": lvl}:
            print("Debug level:", lvl)
        case {"mode": "production"}:
            print("Running in prod mode")
        case _:
            print("Unknown config")
    



  12. Matching Classes (Object Patterns)

  13. class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    def describe(p):
        match p:
            case Point(x=0, y=0):
                return "Origin"
            case Point(x, y):
                return f"Point({x}, {y})"
    



  14. Using Guards (if conditions)

  15. match value:
        case x if x > 0:
            print("Positive")
        case x if x < 0:
            print("Negative")
        case 0:
            print("Zero")
    


  16. Matching Enums

  17. from enum import Enum
    
    class State(Enum):
        READY   = 1
        RUNNING = 2
        STOPPED = 3
    
    match state:
        case State.READY:
            print("Ready")
        case State.RUNNING:
            print("Running")
        case State.STOPPED:
            print("Stopped")
    


  18. Combining Patterns

  19. match event:
        case {"type": "click", "pos": (x, y)}:
            print("Clicked:", x, y)
        case {"type": "keypress", "key": k} if k.isalpha():
            print("Pressed a letter:", k)
        case {"type": "keypress", "key": k}:
            print("Pressed:", k)
        case _:
            print("Unknown event")
    


  20. Common Mistakes

  21. case x = 10   # ❌ invalid
    case 10       # ✔ correct
    

    match value:
        case x:      # ALWAYS matches, captures value into x
            print("Matched:", x)
    

    x = 10
    match value:
        case .x:         # match literal value of x
            print("value is 10")
    


  22. Summary Table

  23. Feature Description
    Value patterns Compare exact literals (200, "ok", etc.)
    OR-patterns "a" | "b" matches either
    Sequence patterns Match lists/tuples, use *rest
    Mapping patterns Match dictionaries by key
    Class patterns Match objects by attribute
    Guards case x if condition
    Wildcard _ matches anything
    Variable capture Patterns bind matched values to variables




Python Numbers

  1. Introduction



  2. Integers (int)

  3. a = 123
    b = -42
    c = 10_000_000  # underscores allowed for readability
    print(a, b, c)
    

    x = 10 ** 100
    print(x)
    


  4. Floating-Point Numbers (float)

  5. x = 3.14
    y = 2.5e6      # scientific notation
    z = float("-inf")
    w = float("nan")
    
    print(x, y, z, w)
    

    print(0.1 + 0.2)   # 0.30000000000000004
    


  6. Complex Numbers (complex)

  7. c = 3 + 4j
    print(c.real)
    print(c.imag)
    

    z1 = 1 + 2j
    z2 = 2 - 3j
    print(z1 + z2)
    print(z1 * z2)
    


  8. Basic Arithmetic Operators

  9. a = 10
    b = 3
    print(a + b)   # addition
    print(a - b)   # subtraction
    print(a * b)   # multiplication
    print(a / b)   # float division
    print(a // b)  # integer division
    print(a % b)   # remainder
    print(a ** b)  # exponentiation
    

    print(-7 // 3)  # -3, because floor(-7/3) = -3
    


  10. Type Conversion

  11. int(3.99)       # 3
    float(10)       # 10.0
    complex(3)      # (3+0j)
    

    int("42")
    float("3.14")
    complex("2+5j")
    


  12. Built-in Numeric Functions
  13. abs(-5)         # 5
    round(3.567, 2) # 3.57
    pow(2, 5)       # 32
    max(1, 4, 2)    # 4
    min(1, 4, 2)    # 1
    


  14. The math Module

  15. import math
    
    print(math.sqrt(16))
    print(math.sin(math.pi / 2))
    print(math.factorial(5))
    



  16. Decimals for High-Precision Calculations

  17. from decimal import Decimal
    
    print(Decimal("0.1") + Decimal("0.2"))  # 0.3 exactly
    


  18. Fractions for Rational Numbers

  19. from fractions import Fraction
    
    x = Fraction(1, 3)
    y = Fraction(1, 6)
    print(x + y)   # 1/2
    


  20. Checking Number Types

  21. isinstance(10, int)
    isinstance(3.14, float)
    isinstance(2 + 3j, complex)
    


  22. Summary

  23. Numeric Type Description
    int Whole numbers with arbitrary precision
    float 64-bit floating-point (may have rounding errors)
    complex Numbers with real and imaginary parts
    Decimal High-precision decimal arithmetic
    Fraction Exact rational numbers
    math Math functions for real numbers
    cmath Math functions for complex numbers



Python Text (Strings)

  1. Introduction



  2. Creating Strings
  3. s1 = "Hello"
    s2 = 'World'
    s3 = "中文也可以"
    s4 = "Emoji 😊 supported"
    

    multiline = """
    This is
    a multiline
    string.
    """
    


  4. String Immutability

  5. s = "hello"
    # s[0] = "H"  # ❌ error
    
    s = "hello"
    s = "H" + s[1:]
    print(s)  # Hello
    


  6. Indexing and Slicing
  7. s = "Python"
    
    print(s[0])     # P
    print(s[-1])    # n
    print(s[1:4])   # yth
    print(s[:3])    # Pyt
    print(s[3:])    # hon
    print(s[::-1])  # reversed
    


  8. Useful String Methods

  9. s = "hello WORLD"
    print(s.lower())
    print(s.upper())
    print(s.title())
    print(s.capitalize())
    

    s = "Python Programming"
    print(s.find("Pro"))   # 7
    print(s.count("m"))    # 2
    

    "  hello  ".strip()    # "hello"
    "  hi  ".lstrip()      # "hi  "
    "  hi  ".rstrip()      # "  hi"
    

    "a-b-c".split("-")          # ['a', 'b', 'c']
    "Hello World".replace("World", "Python")
    

    "-".join(["a", "b", "c"])  # "a-b-c"
    


  10. String Formatting

  11. name = "Hwangfu"
    age = 23
    print(f"My name is {name} and I am {age} years old.")
    

    print("Hello, {}!".format("World"))
    print("{name} is {age}".format(name="Ammy", age=30))
    

    print("%s scored %d points" % ("Ammy", 95))
    


  12. Escape Sequences

  13. print("Line1\nLine2")
    print("Tab\tSeparated")
    print("Quote: \"Hello\"")
    

    r"C:\Users\Name"
    r"\n is not a newline"
    


  14. Unicode Support

  15. s = "你好 😊 Γειά σου"
    print(len(s)) # 13
    

    ord("你")     # Unicode code point
    chr(20320)    # Convert code point to char
    


  16. Bytes vs Strings

  17. b = b"hello"
    print(b[0])         # 104
    

    text = "hello"
    data = text.encode("utf-8")   # bytes
    back = data.decode("utf-8")   # str
    


  18. Substring Testing

  19. s = "hello world"
    
    print("world" in s)     # True
    print(s.startswith("he"))
    print(s.endswith("ld"))
    


  20. Iterating Through a String

  21. for ch in "Python":
        print(ch)
    


  22. String Concatenation

  23. s = "Py" + "thon"
    print(s)
    

    result = "".join(["a"] * 5)
    print(result)   # "aaaaa"
    


  24. Whitespace and Character Classification

  25. s = "abc123"
    
    print(s.isalpha())    # False
    print(s.isdigit())    # False
    print(s.isalnum())    # True
    print(" ".isspace())  # True
    


  26. Summary

  27. Concept Description
    str Unicode text, immutable
    Indexing / slicing Extract individual characters or substrings
    String methods Case changes, searching, replacing, splitting, joining
    Formatting Use f-strings, format(), or %
    Unicode support Strings store Unicode code points
    Bytes vs str Use .encode() and .decode() to convert
    Escape sequences \n, \t, \", and raw strings
    Immutability Strings cannot be modified in place



Python Lists

  1. Introduction



  2. Creating Lists

  3. nums   = [1, 2, 3]
    mixed  = [1, "two", 3.0, True]
    nested = [[1, 2], [3, 4]]
    empty  = []
    
    chars = list("hello")  # ['h', 'e', 'l', 'l', 'o']
    


  4. List Indexing and Slicing

  5. a = [10, 20, 30, 40, 50]
    
    print(a[0])       # 10
    print(a[-1])      # 50
    print(a[1:4])     # [20, 30, 40]
    print(a[:3])      # [10, 20, 30]
    print(a[3:])      # [40, 50]
    print(a[::-1])    # reversed list
    


  6. List Mutability

  7. a = [1, 2, 3]
    a[0] = 100
    print(a)          # [100, 2, 3]
    

    a = [1, 2, 3, 4]
    a[1:3] = [20, 30, 40]
    print(a)          # [1, 20, 30, 40, 4]
    


  8. Adding Elements

  9. a = [1, 2, 3]
    a.append(4)
    print(a)    # [1, 2, 3, 4]
    

    a.insert(1, "X")
    print(a)   # [1, 'X', 2, 3, 4]
    

    a.extend([5, 6])
    print(a)    # [1, 'X', 2, 3, 4, 5, 6]
    


  10. Removing Elements

  11. a = [1, 2, 3, 2]
    a.remove(2)
    print(a)   # [1, 3, 2]
    

    x = a.pop(1)
    print(x)    # removed value
    print(a)    # [1, 2]
    

    a.clear()
    print(a)   # []
    


  12. Searching and Counting

  13. a = [10, 20, 30, 20]
    
    print(a.index(20))   # 1
    print(a.count(20))   # 2
    


  14. Sorting and Reversing

  15. a = [3, 1, 4, 1, 5]
    
    a.sort()
    print(a)    # [1, 1, 3, 4, 5]
    
    a.sort(reverse=True)
    print(a)    # [5, 4, 3, 1, 1]
    

    b = sorted(a)
    

    a.reverse()
    


  16. Iterating Over a List

  17. for x in [10, 20, 30]:
        print(x)
    

    for i, val in enumerate(["a", "b", "c"]):
        print(i, val)
    


  18. Membership Testing

  19. a = [1, 2, 3]
    
    print(2 in a)      # True
    print(10 not in a) # True
    


  20. List Comprehensions

  21. squares = [x * x for x in range(5)]
    evens = [x for x in range(20) if x % 2 == 0]
    
    print(squares)
    print(evens)
    

    matrix = [[i * j for j in range(3)] for i in range(3)]
    print(matrix)
    


  22. Copying Lists

  23. a = [1, 2, 3]
    b = a[:]        # shallow copy
    c = list(a)     # shallow copy
    

    import copy
    
    nested = [[1], [2]]
    d = copy.deepcopy(nested)
    


  24. Common List Pitfall: Shared References

  25. a = [[0] * 3] * 3
    print(a)
    a[0][0] = 99
    print(a)  # all rows changed!
    

    a = [[0 for _ in range(3)] for _ in range(3)]
    


  26. Summary

  27. Feature Description
    Ordered List elements maintain insertion order
    Mutable Elements and slices can be changed in place
    Heterogeneous Can contain mixed types
    Dynamic Lists grow and shrink automatically
    List methods append, insert, extend, remove, pop, clear, sort, reverse
    Comprehensions Powerful syntax for generating lists
    Copying Use slice or copy() for shallow; deepcopy() for deep
    Common pitfall [[0] * n] * m duplicates references



Python Type Annotations

  1. Introduction



  2. Basic Function Annotations

  3. def add(x: int, y: int) -> int:
        return x + y
    

    def greet(name: str, age: int) -> str:
        return f"{name} is {age} years old"
    


  4. Annotating Variables

  5. age: int = 25
    name: str = "Hwangfu"
    height: float = 1.80
    

    count: int
    


  6. Common Built-in Types

  7. n: int = 10
    flag: bool = True
    ratio: float = 0.75
    text: str = "Hello"
    


  8. Containers and Generics

  9. nums: list[int] = [1, 2, 3]
    names: set[str] = {"a", "b"}
    mapping: dict[str, int] = {"a": 1}
    matrix: list[list[int]] = [[1, 2], [3, 4]]
    



  10. Optional Types

  11. from typing import Optional
    
    age: Optional[int] = None
    

    age: int | None = None
    


  12. Union Types

  13. from typing import Union
    
    value: Union[int, str] = 10
    

    value: int | str = 10
    


  14. Any Type

  15. from typing import Any
    
    data: Any = "hello"
    data = 123         # allowed
    


  16. Literal Types

  17. from typing import Literal
    
    mode: Literal["debug", "prod", "test"] = "debug"
    


  18. Callable / Function Types

  19. from typing import Callable
    
    def apply(f: Callable[[int, int], int], x: int, y: int) -> int:
        return f(x, y)
    


  20. Tuple Types

  21. coords: tuple[int, int] = (10, 20)
    point: tuple[int, str, float] = (1, "a", 1.5)
    

    nums: tuple[int, ...] = (1, 2, 3)
    


  22. Self Type (Methods returning instance)

  23. from typing import Self
    
    class Vector:
        def scale(self, x: float) -> Self:
            self.x *= x
            self.y *= x
            return self
    


  24. TypedDict (for dicts with fixed structure)

  25. from typing import TypedDict
    
    class User(TypedDict):
        id: int
        name: str
        email: str
    
    u: User = {"id": 1, "name": "Ammy", "email": "a@example.com"}
    


  26. Protocol (Duck Typing Interfaces)

  27. from typing import Protocol
    
    class Drawable(Protocol):
        def draw(self) -> None:
            ...
    
    def render(obj: Drawable) -> None:
        obj.draw()
    



  28. Final

  29. from typing import Final
    
    PI: Final = 3.14159
    


  30. Class and Instance Attributes

  31. class Person:
        name: str          # instance attribute annotation
        count: int = 0     # class variable annotation
    


  32. Forward References

  33. class Node:
        next: "Node" | None
    


  34. Union Return Patterns with match

  35. def parse(data: str) -> int | float | None:
        match data:
            case "none":
                return None
            case _ if data.isdigit():
                return int(data)
            case _:
                return float(data)
    


  36. ParamSpec and TypeVar (Advanced Generics)

  37. from typing import TypeVar, ParamSpec, Callable
    
    P = ParamSpec("P")
    R = TypeVar("R")
    
    def wrapper(f: Callable[P, R]) -> Callable[P, R]:
        def inner(*args: P.args, **kwargs: P.kwargs) -> R:
            return f(*args, **kwargs)
        return inner
    


  38. Annotated (Metadata for Types)

  39. from typing import Annotated
    
    x: Annotated[int, "must be positive"] = 5
    


  40. Type Aliases

  41. UserId = int
    Vector = list[float]
    
    def scale(v: Vector) -> Vector:
        return [x * 10 for x in v]
    


  42. Runtime Access to Annotations

  43. def f(x: int, y: str) -> bool:
        return True
    
    print(f.__annotations__)
    


  44. Summary of Major Annotation Features

  45. Feature Description
    Basic types int, float, bool, str
    Generics list[int], dict[str, int]
    Optional int | None
    Union int | str
    Any Disable type checking
    Literal Restrict to specific values
    Callable Describe function signatures
    TypedDict Dicts with fixed field types
    Protocol Duck-typing interfaces
    Final Constant-like values
    Self Methods returning class instance
    ParamSpec / TypeVar Advanced generic functions
    Annotated Add metadata to types
    Type Aliases Give names to complex types



Python Protocols

  1. Introduction



  2. Basic Protocol Example

  3. from typing import Protocol
    
    class Greeter(Protocol):
        def greet(self) -> str:
            ...
    
    class Person:
        def greet(self) -> str:
            return "Hello!"
    
    class Robot:
        def greet(self) -> str:
            return "Beep bop"
    

    def welcome(g: Greeter) -> None:
        print(g.greet())
    
    welcome(Person())   # OK
    welcome(Robot())    # OK
    


  4. No Need to Inherit

  5. class Cat:
        def greet(self) -> str:
            return "Meow"
    
    welcome(Cat())   # Also OK
    



  6. Protocols with Attributes

  7. class User(Protocol):
        name: str
        age: int
    
    def print_user(u: User) -> None:
        print(u.name, u.age)
    
    class Person:
        def __init__(self, name: str, age: int) -> None:
            self.name = name
            self.age = age
    
    print_user(Person("Hwangfu", 23))
    


  8. Protocols with Multiple Methods

  9. class FileLike(Protocol):
        def read(self) -> str:
            ...
        def write(self, s: str) -> int:
            ...
    


  10. Extending Protocols

  11. class Shape(Protocol):
        def area(self) -> float:
            ...
    
    class DrawableShape(Shape, Protocol):
        def draw(self) -> None:
            ...
    


  12. Optional Protocol Features

  13. from typing import runtime_checkable
    
    @runtime_checkable
    class Greeter(Protocol):
        def greet(self) -> str:
            ...
    

    print(isinstance(Person(), Greeter))   # True
    print(isinstance(42, Greeter))         # False
    


  14. Protocols vs Abstract Base Classes (ABC)

  15. Feature Protocol ABC (Abstract Base Class)
    Typing style Structural (duck typing) Nominal (inherits from ABC)
    Requires inheritance? No Yes
    Runtime enforcement No (unless using @runtime_checkable) Yes (raises error if abstract methods not implemented)
    Common usage Flexible APIs, plugin systems, duck typing Strict class hierarchies
    Better for static type checking? Yes Partly


  16. Protocol with Generics

  17. from typing import Protocol, TypeVar
    
    T = TypeVar("T")
    
    class Container(Protocol[T]):
        def add(self, item: T) -> None:
            ...
    


  18. Using Protocols to Define Function Signatures

  19. class Comparator(Protocol):
        def __call__(self, a: int, b: int) -> bool:
            ...
    
    def sort_with(nums: list[int], comp: Comparator) -> None:
        nums.sort(key=lambda x: x, reverse=comp(1, 0))
    


  20. Protocols with Properties

  21. class Sized(Protocol):
        @property
        def size(self) -> int:
            ...
    
    class Box:
        def __init__(self, size: int) -> None:
            self._size = size
    
        @property
        def size(self) -> int:
            return self._size
    


  22. Summary

  23. Concept Description
    Protocol Defines a structural-typing interface
    No inheritance required Matches by methods/attributes present
    @runtime_checkable Allows isinstance() with Protocol
    Attributes & Methods Protocols can specify both
    Generics support Protocols can accept type parameters
    Use cases Flexible interfaces, APIs, plugin architectures
    Difference from ABC Structural vs. nominal typing



Python TypedDict

  1. Introduction



  2. Basic Usage

  3. from typing import TypedDict
    
    class User(TypedDict):
        id: int
        name: str
        email: str
    

    u: User = {
        "id": 1,
        "name": "Ammy",
        "email": "alice@example.com"
    }
    


  4. TypedDict Enforces Key Presence

  5. bad_user: User = {
        "id": 1,
        "name": "Ammy"
    }   # ❌ error: missing key "email"
    

    wrong: User = {
        "id": 1,
        "name": "Ammy",
        "email": "a@x.com",
        "age": 23
    }   # ❌ error: extra key "age"
    


  6. Optional Keys

  7. class User(TypedDict):
        id: int
        name: str
        email: str | None   # value may be None
    

    class PartialUser(TypedDict, total=False):
        id: int
        name: str
        email: str
    

    p: PartialUser = {}
    


  8. Mixing Required and Optional Keys

  9. class User(TypedDict):
        id: int
        name: str
    
    class UserExtra(TypedDict, total=False):
        email: str
        age: int
    
    class User(TypedDict):
        id: int
        name: str
        email: str | None   # optional value, key still required
    


  10. Readonly Keys (Python 3.12+)

  11. from typing import TypedDict, NotRequired, ReadOnly
    
    class Config(TypedDict):
        version: ReadOnly[int]
        debug: NotRequired[bool]
    




  12. TypedDict with Inheritance

  13. class BaseUser(TypedDict):
        id: int
        name: str
    
    class FullUser(BaseUser):
        email: str
        age: int
    




  14. TypedDict vs Dataclass

  15. Feature TypedDict Dataclass
    Runtime type Just a dict Custom class
    Allows missing keys? No (unless total=False) Yes (default values)
    Fast creation? Yes (dict) Slower (object)
    Attribute access Key-based (d["key"]) Dot-access (d.key)
    Static type checking Strong Strong
    Serialization (JSON) Easy Requires conversion


  16. TypedDict with Functions

  17. def process_user(user: User) -> None:
        print(user["id"], user["name"])
    


  18. TypedDict for API Responses

  19. class ApiResponse(TypedDict):
        status: int
        data: dict[str, str]
    
    response: ApiResponse = {
        "status": 200,
        "data": {"id": "123"}
    }
    


  20. Nesting TypedDicts

  21. class Profile(TypedDict):
        bio: str
        github: str
    
    class User(TypedDict):
        id: int
        profile: Profile
    


  22. TypedDict with Literal Types

  23. from typing import Literal
    
    class Event(TypedDict):
        type: Literal["click", "keypress"]
        payload: dict[str, str]
    


  24. Total=False Default Behavior

  25. class Partial(TypedDict, total=False):
        required: int
        # everything here is optional
    


  26. Using TypedDict with **kwargs

  27. def create_user(**kwargs: User) -> User:
        return kwargs
    


  28. Runtime Representation

  29. u: User = {"id": 1, "name": "Ammy", "email": "a@x.com"}
    
    print(type(u))    # <class 'dict'>
    


  30. Summary

  31. Concept Description
    TypedDict Dictionary with predefined keys and value types
    Required keys All keys required unless total=False
    Optional keys Via total=False or optional values
    Runtime representation Still a plain dict, only static checks apply
    Supports inheritance Extend dictionary definitions
    Common use cases API schemas, config objects, JSON-like data



Python Type Checkers: mypy, pyright, and ruff

  1. Introduction



  2. What Static Type Checking Does

  3. def add(a: int, b: int) -> int:
        return a + b
    
    add("hi", 3)  # ❌ static type error
    


  4. mypy

  5. pip install mypy
    

    mypy app.py
    

    [mypy]
    strict = True
    disallow_untyped_defs = True
    warn_unused_ignores = True
    


  6. pyright

  7. npm install -g pyright
    

    pyright
    

    {
      "include": ["src"],
      "strict": true
    }
    


  8. ruff

  9. pip install ruff
    

    ruff check .
    

    [tool.ruff]
    select = ["E", "F", "I", "TCH"]
    


  10. Comparison Table

  11. Feature mypy pyright ruff
    Checking depth Very deep Very deep Shallow
    Speed Medium Very fast Extremely fast
    Best use case Strict typing, CI validation Daily development, IDE Linting + basic type checks
    Protocol support Excellent Excellent Minimal
    TypedDict analysis Strong Strong Partial


  12. Summary

  13. Tool Description
    mypy Classic strict type checker with full feature support
    pyright Fast, modern, editor-focused type checker
    ruff Ultra-fast linter with optional lightweight type warnings



Special Parameters in Python Functions

  1. Introduction



  2. Positional-Only Parameters (/)

  3. def add(a, b, /):
        return a + b
    
    add(3, 4)        # OK
    add(a=3, b=4)    # ❌ error: cannot use keyword args
    



  4. Keyword-Only Parameters (*)

  5. def configure(*, debug=False, verbose=False):
        print(debug, verbose)
    
    configure(debug=True)     # OK
    configure(True, True)     # ❌ error
    



  6. Combining Positional-Only and Keyword-Only

  7. def func(a, b, /, c, d, *, e, f):
        print(a, b, c, d, e, f)
    


  8. Variable Positional Arguments (*args)

  9. def total(*numbers):
        print(numbers)
    
    total(1, 2, 3)      # (1, 2, 3)
    total()             # ()
    


  10. Variable Keyword Arguments (**kwargs)

  11. def debug(**options):
        print(options)
    
    debug(level=3, verbose=True)
    # {'level': 3, 'verbose': True}
    


  12. Using *args and **kwargs Together

  13. def func(a, *args, **kwargs):
        print(a, args, kwargs)
    
    func(1, 2, 3, x=10, y=20)
    

    1 (2, 3) {'x': 10, 'y': 20}
    


  14. Enforcing Only **kwargs Arguments

  15. def build_model(*, layers, activation):
        ...
    

    build_model(3, "relu")   # ❌
    


  16. Using / with *args and **kwargs

  17. def f(a, b, /, c, *args, d, e=0, **kwargs):
        print(a, b, c, args, d, e, kwargs)
    



  18. Real Example: Built-in pow

  19. pow(base, exp, mod=None, /)
    



  20. Common Uses in Real Codebases



  21. Summary

  22. Syntax Meaning
    / Positional-only parameters
    * Keyword-only parameters
    *args Captures extra positional arguments
    **kwargs Captures extra keyword arguments
    *args, **kwargs Flexible wrappers for arbitrary signatures



Python range

  1. Introduction



  2. Basic Forms of range

  3. range(5)            # 0, 1, 2, 3, 4
    range(2, 7)         # 2, 3, 4, 5, 6
    range(1, 10, 2)     # 1, 3, 5, 7, 9
    


  4. Range Objects Don’t Produce Lists

  5. list(range(5))
    # [0, 1, 2, 3, 4]
    


  6. Default Behavior

  7. range(4)     # 0, 1, 2, 3  (never reaches 4)
    


  8. Using range in Loops

  9. for i in range(3):
        print(i)
    
    0
    1
    2
    


  10. Using range with start and stop

  11. for i in range(5, 10):
        print(i)
    


  12. Using step

  13. range(0, 10, 2)   # even numbers: 0,2,4,6,8
    range(1, 10, 2)   # odd numbers: 1,3,5,7,9
    


  14. Negative Step (Counting Backward)

  15. range(10, 0, -1)     # 10, 9, 8, ..., 1
    range(5, -6, -2)     # 5, 3, 1, -1, -3, -5
    



  16. Ranges with Zero or Invalid Steps

  17. range(0, 10, 0)    # ❌ error: step cannot be zero
    


  18. Range is Lazy and Efficient

  19. r = range(1_000_000_000)
    print(len(r))      # works instantly!
    


  20. Membership Testing (in)

  21. 5 in range(10)         # True
    9 in range(1, 10, 2)   # False
    


  22. Indexing and Slicing

  23. r = range(10, 20)
    print(r[0])    # 10
    print(r[5])    # 15
    print(r[-1])   # 19
    

    r = range(0, 20, 2)  # 0,2,4,...18
    print(r[2:6])         # range(4, 12, 2)
    


  24. Common Patterns with range

  25. for _ in range(3):
        print("hi")
    

    names = ["Anna", "Ben", "Carl"]
    for i in range(len(names)):
        print(i, names[i])
    

    for i in range(10, -1, -1):
        print(i)
    

    evens = list(range(0, 21, 2))
    


  26. Comparison of List vs Range

  27. Operation list(range) range
    Memory usage Large (stores all values) Tiny (three integers only)
    Speed of iteration Fast Fast
    Supports slicing Yes Yes
    Supports membership test Linear search Constant-time math


  28. Summary

  29. Feature Description
    Lazy No list stored; values computed when needed
    Efficient Constant memory usage
    Exclusive stop Sequence stops before the stop value
    Step Can count upward or downward
    Supports slicing Slicing returns a new range
    Constant-time membership Efficient mathematical check



Python Lambda Expressions

  1. Introduction



  2. Basic Syntax

  3. lambda arguments: expression
    

    add = lambda a, b: a + b
    print(add(3, 4))   # 7
    

    def add(a, b):
        return a + b
    


  4. Single Expression Only

  5. lambda x: x * 2           # OK
    lambda x: print(x)        # OK (print is an expression here)
    
    lambda x: y = x + 1       # ❌ error (assignment is not allowed)
    lambda x: for i in ...    # ❌ cannot contain loops
    


  6. Zero-Argument Lambdas

  7. noop = lambda: None
    noop()
    


  8. Lambdas with Default Values

  9. f = lambda x=10, y=20: x + y
    print(f())       # 30
    print(f(5))      # 25
    


  10. Lambdas Returning Other Lambdas

  11. make_multiplier = lambda n: (lambda x: x * n)
    times3 = make_multiplier(3)
    print(times3(10))   # 30
    


  12. Lambdas in sorted()

  13. names = ["alice", "Bob", "carol"]
    sorted_names = sorted(names, key=lambda s: s.lower())
    print(sorted_names)
    


  14. Lambdas in map()

  15. nums = [1, 2, 3]
    doubled = list(map(lambda n: n * 2, nums))
    print(doubled)   # [2, 4, 6]
    


  16. Lambdas in filter()

  17. nums = [1, 2, 3, 4, 5]
    evens = list(filter(lambda n: n % 2 == 0, nums))
    print(evens)     # [2, 4]
    


  18. Lambdas in reduce()

  19. from functools import reduce
    
    nums = [1, 2, 3, 4]
    total = reduce(lambda a, b: a + b, nums)
    print(total)     # 10
    


  20. Lambdas in Dictionary Structures

  21. actions = {
        "square": lambda x: x * x,
        "cube": lambda x: x * x * x,
    }
    
    print(actions)   # 27
    


  22. Using Lambdas in UI / Callback Code

  23. def on_click(callback):
        callback()
    
    on_click(lambda: print("Clicked!"))
    


  24. Using Lambdas with Closures

  25. def make_add(n):
        return lambda x: x + n
    
    add5 = make_add(5)
    print(add5(10))   # 15
    


  26. Lambdas vs Named Functions

  27. Feature lambda def function
    Has a name? No Yes
    Multiple statements? No Yes
    Main usage Short, throwaway functions Larger or reusable logic
    Debug readability Poor Good
    Syntax Single expression Block with return


  28. Summary

  29. Concept Description
    Anonymous lambda defines short unnamed functions
    Single expression No blocks, no loops, no assignments
    Common uses sorted, map, filter, callbacks
    Readability Good for small tasks, bad for complex logic
    Alternatives Use def for named, clearer functions



Python Documentation Strings (Docstrings)

  1. Introduction

  2. """This is a docstring."""


  3. Where Docstrings Can Be Placed

  4. """
    This module handles user authentication logic.
    """
    
    
    def login(user, password):
        """Authenticate a user by password."""
        ...
    


  5. Accessing Docstrings

  6. print(login.__doc__)
    help(login)
    



  7. One-Line Docstrings

  8. def add(a, b):
        """Return the sum of a and b."""
        return a + b
    


  9. Multi-Line Docstrings

  10. def connect(url, timeout=10):
        """
        Connect to a remote server.
    
        This function establishes a TCP connection to the given URL.
        The connection will fail if the timeout is exceeded.
        """
        ...
    


  11. Docstrings for Classes

  12. class User:
        """
        Represents a system user with username and email.
        """
    
        def __init__(self, username, email):
            """Initialize a new User object."""
            self.username = username
            self.email = email
    


  13. Docstrings for Methods

  14. class Circle:
        """Circle with radius and area calculation."""
    
        def area(self):
            """Return the area of the circle."""
            import math
            return math.pi * self.r * self.r
    


  15. Documenting Parameters



  16. Google Style Example
  17. def add(a, b):
        """
        Add two numbers.
    
        Args:
            a (int): First number.
            b (int): Second number.
    
        Returns:
            int: Sum of a and b.
        """
        return a + b
    


  18. NumPy Style

  19. def scale(values, factor):
        """
        Scale an array by a factor.
    
        Parameters
        ----------
        values : list[int]
            Sequence of numbers.
        factor : int or float
            Multiplier.
    
        Returns
        -------
        list[int]
            Scaled numbers.
        """
        return [v * factor for v in values]
    


  20. Sphinx / reStructuredText (RST) Style

  21. def greet(name):
        """
        Greet a user.
    
        :param name: User's name.
        :type name: str
        :return: Greeting message.
        :rtype: str
        """
        return f"Hello, {name}!"
    


  22. Documenting Return Values

  23. def compute(x):
        """
        Compute something useful.
    
        Returns:
            float: The computed value.
        """
        return x * 2.5
    


  24. Documenting Exceptions

  25. def read_file(path):
        """
        Read a file.
    
        Args:
            path (str): File path.
    
        Raises:
            FileNotFoundError: If the file does not exist.
        """
        with open(path) as f:
            return f.read()
    


  26. Docstrings for Modules

  27. """
    utility.py - helper functions for math and statistics.
    """
    


  28. Docstrings for Packages

  29. """
    This package contains utilities for data analysis.
    """
    


  30. Automated Tools That Use Docstrings



  31. PEP 257 Docstring Conventions



  32. Summary

  33. Aspect Description
    Definition String literal used to document modules, classes, and functions
    Supported By help(), IDEs, Sphinx, pydoc
    Styles Google, NumPy, Sphinx/RST
    Retrieval object.__doc__ or help()
    Purpose Provide human-readable API documentation
    Best Practices Short summary + details, parameter docs, return/exception docs



Python Function Annotations

  1. Introduction



  2. Basic Syntax

  3. def add(a: int, b: int) -> int:
        return a + b
    



  4. Accessing Annotations at Runtime

  5. print(add.__annotations__)
    
    {
        'a': int,
        'b': int,
        'return': int
    }
    


  6. Annotations Are Not Enforced

  7. add("hello", "world")   # Works (but might not be meaningful)
    



  8. Annotating Optional and Default Arguments

  9. def greet(name: str = "World") -> str:
        return f"Hello, {name}!"
    



  10. Annotating *args and **kwargs

  11. def func(*args: int, **kwargs: str) -> None:
        pass
    



  12. Forward References

  13. def parse(user: "User") -> "Result":
        ...
    



  14. Using Annotations Beyond Types

  15. def process(x: "database_id", y: list) -> "status":
        pass
    



  16. Annotating Collections

  17. def average(nums: list[float]) -> float:
        return sum(nums) / len(nums)
    


  18. Union Types

  19. def load(config: str | None) -> str:
        return config or "default"
    



  20. Callable Annotations

  21. from typing import Callable
    
    def apply(f: Callable[[int, int], int], x: int, y: int) -> int:
        return f(x, y)
    


  22. Using Annotated for Metadata

  23. from typing import Annotated
    
    def scale(x: Annotated[int, "positive"]) -> int:
        return x
    



  24. Return Type of None

  25. def log(msg: str) -> None:
        print(msg)
    


  26. Typical Misunderstandings

  27. Misunderstanding Explanation
    "Annotations enforce types" They do not — Python ignores them at runtime unless tools check them.
    "Annotations must be types" No — any object is allowed.
    "Annotations slow down code" No — they are just stored metadata.


  28. Summary

  29. Feature Description
    Syntax param: type and -> return_type
    Runtime access func.__annotations__
    Enforcement None by Python itself
    Purpose Documentation, type checking, IDE support
    Advanced tools mypy, pyright, runtime frameworks
    Supported types Any object — not just types



Python Tuples and Sequences

  1. Introduction



  2. What Is a Tuple?

  3. t = (1, 2, 3)
    


  4. Creating Tuples

  5. # Parentheses (typical)
    t1 = (1, 2, 3)
    
    # Without parentheses (tuple packing)
    t2 = 4, 5, 6
    
    # Empty tuple
    t3 = ()
    
    # Single-element tuple (comma is required!)
    single = (42,)
    



  6. Tuple Indexing and Slicing

  7. t = (10, 20, 30, 40)
    
    print(t[0])   # 10
    print(t[-1])  # 40
    
    print(t[1:3]) # (20, 30)
    


  8. Immutability

  9. t = (1, 2, 3)
    t[1] = 99     # ❌ TypeError
    

    t = ([1, 2], 3)
    t[0].append(99)
    print(t)      # ([1, 2, 99], 3)
    


  10. Tuple Unpacking

  11. x, y, z = (10, 20, 30)
    
    print(x)  # 10
    print(y)  # 20
    print(z)  # 30
    


  12. Extended Unpacking (PEP 3132)

  13. a, *b = (1, 2, 3, 4)
    print(a)  # 1
    print(b)  # [2, 3, 4]
    
    *a, b = (1, 2, 3, 4)
    print(a)  # [1, 2, 3]
    print(b)  # 4
    


  14. Tuples as Return Values

  15. def stats():
        return (10, 20, 30)
    
    x, y, z = stats()
    



  16. Why Use Tuples Instead of Lists?

  17. Reason Explanation
    Immutability Protects data from accidental changes
    Hashability Tuples can be used as dict keys (if elements are hashable)
    Performance Slightly faster and smaller than lists
    Semantic meaning Represents a fixed structure, like a row or coordinate


  18. Tuple Methods

  19. t.count(value)
    t.index(value)
    


  20. Nested and Structured Tuples

  21. point = (10, 20)
    rect = (point, (30, 40))  # nested
    


  22. Sequences: Common Operations

  23. seq = (1, 2, 3)
    print(len(seq))
    print(2 in seq)
    print(seq + (4, 5))
    print(seq * 2)
    


  24. Tuple vs Sequence vs Iterable

  25. Concept Meaning
    Iterable Anything usable in a for loop
    Sequence Ordered iterable with indexing and slicing
    Tuple An immutable sequence


  26. Tuple Type Annotations

  27. # fixed size
    point: tuple[int, int] = (10, 20)
    
    # variable length
    numbers: tuple[int, ...] = (1, 2, 3, 4)
    


  28. Common Use Cases of Tuples



  29. Summary

  30. Feature Tuple
    Mutability Immutable
    Ordering Preserved
    Indexing / slicing Supported
    Performance Faster and smaller than lists
    Common use Fixed-size structures, multiple return values
    Hashable? Yes, if all elements are hashable



The del Statement in Python

  1. Introduction



  2. Deleting Variables (Name Bindings)

  3. x = 10
    del x
    
    print(x)   # NameError: name 'x' is not defined
    



  4. Deleting Multiple Names

  5. a = 1
    b = 2
    c = 3
    
    del a, b, c
    


  6. Deleting List Elements

  7. nums = [10, 20, 30, 40]
    del nums[1]
    
    print(nums)   # [10, 30, 40]
    



  8. Deleting List Slices

  9. nums = [1, 2, 3, 4, 5]
    del nums[1:4]
    
    print(nums)   # [1, 5]
    



  10. Deleting Dictionary Keys

  11. person = {"name": "Ammy", "age": 30}
    del person["age"]
    
    print(person)   # {"name": "Ammy"}
    



  12. Deleting Attributes

  13. class User:
        def __init__(self):
            self.name = "Ammy"
            self.age = 20
    
    u = User()
    del u.age
    
    print(u.age)   # AttributeError
    



  14. Deleting Items in Nested Structures

  15. data = {"users": ["alice", "bob", "carol"]}
    del data["users"][1]
    
    print(data)   # {"users": ["alice", "carol"]}
    


  16. Using del to Reduce Memory Pressure

  17. import pandas as pd
    
    df = pd.read_csv("big.csv")
    # ... process ...
    
    del df   # allow Python to free memory sooner
    


  18. del Does Not Force Immediate Garbage Collection

  19. x = [1, 2, 3]
    y = x
    del x
    
    print(y)   # [1, 2, 3] — object still exists!
    


  20. Using del in Loops

  21. items = [1, 2, 3, 4, 5]
    
    for i in range(len(items)):
        del items[0]
    
    print(items)  # []
    



  22. del vs pop() vs remove()

  23. Operation Use Case Returns value? Errors?
    del seq[i] Delete by index No IndexError
    seq.pop(i) Delete by index Yes (returns value) IndexError
    seq.remove(x) Delete first matching value No ValueError


  24. The __del__() Method (Destructor)

  25. class A:
        def __del__(self):
            print("Object destroyed")
    



  26. Common Pitfalls

  27. Pitfall Description
    Deleting wrong slice del seq[a:b] can remove more than intended
    Deleting inside loops Can change indices and cause skipped items
    Expecting memory to clear immediately Python may retain object until all references are gone
    Confusing del with destruction del removes names, not objects


  28. Summary

  29. Feature Description
    Main Role Remove bindings between names and objects
    Can delete Variables, list items, dictionary keys, object attributes, slices
    Does not do Force garbage collection or memory cleanup
    Namespace effect After del, the name is gone
    Memory Object disappears only when no references remain



Python Sets

  1. Introduction



  2. Creating Sets

  3. s = {1, 2, 3}
    empty = set()      # correct way
    

    not_set = {}   # this is a dict
    

    # Using constructor with any iterable
    s = set([1, 2, 3, 3])   # duplicates removed
    print(s)                # {1, 2, 3}
    


  4. Basic Properties

  5. Property Description
    No ordering Elements appear in arbitrary order
    No indexing You cannot do s[0]
    Unique items Duplicates are removed automatically
    Fast membership in runs in constant time on average


  6. Adding and Removing Elements

  7. s = {1, 2, 3}
    
    s.add(4)         # {1, 2, 3, 4}
    s.remove(2)      # {1, 3, 4}
    

    s.discard(10)    # safe: no error
    

    x = s.pop()
    print(x)        # unpredictable which element
    


  8. Membership Testing

  9. if 3 in s:
        print("Found!")
    


  10. Eliminating Duplicates from a List

  11. nums = [1, 2, 2, 3, 3, 3]
    unique = list(set(nums))
    


  12. Mathematical Set Operations

  13. A = {1, 2, 3}
    B = {3, 4, 5}
    

    A | B       # {1, 2, 3, 4, 5}
    A.union(B)
    

    A & B       # {3}
    A.intersection(B)
    

    A - B       # {1, 2}
    A.difference(B)
    

    A ^ B       # {1, 2, 4, 5}
    A.symmetric_difference(B)
    

    A <= B     # subset?
    A.issubset(B)
    
    A >= B     # superset?
    A.issuperset(B)
    


  14. Mutable vs Immutable Sets

  15. fs = frozenset([1, 2, 3])
    

    Type Mutable? Hashable? Usable as dict key?
    set Yes No No
    frozenset No Yes Yes


  16. Iteration Over Sets

  17. for x in {1, 2, 3}:
        print(x)
    



  18. Set Comprehensions

  19. s = {x * 2 for x in range(5)}
    print(s)     # {0, 2, 4, 6, 8}
    


  20. Performance Characteristics

  21. Operation Average Complexity
    x in s O(1)
    s.add(x) O(1)
    s.remove(x) O(1)
    Union / intersection O(n)


  22. Restrictions

  23. {[1, 2, 3]}   # ❌ TypeError (lists are not hashable)
    

    {(1, 2, 3)}   # ✔ tuples are hashable (if contents are)
    


  24. Common Use Cases



  25. Summary

  26. Feature Description
    Main characteristics Unordered, unique, hashable elements
    Mutable? set: yes; frozenset: no
    Indexable? No
    Fast lookup? Yes (O(1))
    Common operations Union, intersection, difference, membership
    Use cases Deduplication, analysis, membership test, algorithms



Python Dictionaries

  1. Introduction



  2. Creating Dictionaries

  3. # Literal syntax
    d = {"name": "Ammy", "age": 30}
    
    # Using dict()
    d = dict(name="Ammy", age=30)
    
    # From sequence of pairs
    d = dict([("name", "Ammy"), ("age", 30)])
    
    # Empty dictionary
    empty = {}
    


  4. Dictionary Keys

  5. {"a": 1}                # ok
    {(1, 2): "point"}       # ok
    {[1, 2]: "bad"}         # ❌ TypeError
    


  6. Accessing and Modifying Values

  7. person = {"name": "Ammy", "age": 30}
    
    print(person["name"])     # "Ammy"
    
    person["age"] = 31        # modify
    person["city"] = "Berlin" # add new
    


  8. Membership Testing

  9. "age" in person        # True
    "Berlin" in person     # False
    


  10. Safe Access with get()

  11. person.get("age")          # 31
    person.get("country")      # None
    person.get("country", "?") # "?"
    


  12. Removing Items

  13. person.pop("age")          # returns the value
    person.pop("missing", "?") # default if key missing
    
    del person["name"]         # raises KeyError if missing
    
    person.clear()             # remove all items
    


  14. Dictionary Views: keys(), values(), items()

  15. d = {"a": 1, "b": 2}
    
    d.keys()    # dict_keys(['a', 'b'])
    d.values()  # dict_values([1, 2])
    d.items()   # dict_items([('a', 1), ('b', 2)])
    



  16. Iterating Over Dictionaries

  17. for key in d:
        print(key)
    
    for value in d.values():
        print(value)
    
    for key, value in d.items():
        print(key, value)
    


  18. Dictionary Comprehensions

  19. squares = {x: x * x for x in range(5)}
    # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
    


  20. Merging Dictionaries

  21. a = {"x": 1, "y": 2}
    b = {"y": 3, "z": 4}
    
    c = a | b
    # {'x': 1, 'y': 3, 'z': 4}
    
    d = {**a, **b}
    


  22. Default Dictionary Values

  23. counts = {}
    counts.setdefault("apple", 0)
    counts["apple"] += 1
    



  24. Using collections.defaultdict

  25. from collections import defaultdict
    
    counts = defaultdict(int)
    
    counts["apple"] += 1
    counts["apple"] += 1
    



  26. Dictionaries Are Mutable

  27. d = {"a": 1}
    e = d
    e["a"] = 99
    
    print(d)  # {"a": 99}
    

    copy_d = d.copy()        # shallow copy
    import copy
    deep_d = copy.deepcopy(d)
    


  28. Nested Dictionaries

  29. users = {
        "alice": {"age": 20, "active": True},
        "bob":   {"age": 30, "active": False},
    }
    


  30. Restrictions on Keys

  31. # OK
    {(1, 2): "point"}
    
    # Error
    {[1, 2]: "list"}     # TypeError
    


  32. Dictionary Performance Overview

  33. Operation Average Complexity
    Lookup d[k] O(1)
    Insert / replace O(1)
    Delete O(1)
    Iteration O(n)


  34. Common Use Cases



  35. Summary

  36. Feature Description
    Type Mutable mapping of key–value pairs
    Key requirements Hashable, unique
    Value requirements Anything allowed
    Common operations Lookup, insertion, deletion, iteration
    Performance O(1) average lookup and insert
    Extensions defaultdict, OrderedDict, Counter



Python Modules: Introduction

  1. What Is a Module?



  2. Creating Your Own Module

  3. # greetings.py
    def hello(name):
        return f"Hello, {name}!"
    
    PI = 3.14159
    

    import greetings
    
    print(greetings.hello("Ammy"))
    print(greetings.PI)
    


  4. Importing Modules

  5. import math
    print(math.sqrt(16))
    

    from math import sqrt
    print(sqrt(25))
    

    from math import sin, cos, pi
    

    from math import *
    


  6. Module Aliases

  7. import math as m
    
    print(m.pi)
    


  8. Where Python Looks for Modules

  9. import sys
    print(sys.path)
    


  10. The __name__ Variable

  11. python greetings.py
    

    # inside greetings.py
    print(__name__)  # "__main__"
    

    import greetings
    print(greetings.__name__)   # "greetings"
    


  12. Running Code Only When Module Is Executed

  13. # greetings.py
    
    def hello(name):
        return f"Hello, {name}!"
    
    if __name__ == "__main__":
        print(hello("Tester"))
    



  14. Standard Library Modules

  15. import json
    print(json.dumps({"a": 1}))
    


  16. Third-Party Modules

  17. pip install requests
    

    import requests
    print(requests.get("https://example.com").status_code)
    


  18. Packages vs Modules

  19. Term Meaning
    Module A single .py file
    Package A directory containing modules (with __init__.py inside)


  20. Importing from Packages

  21. # project structure:
    # utils/
    #   __init__.py
    #   math_tools.py
    
    from utils.math_tools import add
    


  22. Reloading Modules

  23. import importlib
    importlib.reload(greetings)
    




Python vs Python3

  1. Introduction



  2. Command-Line Differences

  3. python      # might run Python 2.7
    python3     # runs Python 3.x
    

    python      # runs Python 3.x
    python3     # also runs Python 3.x
    

    pip     # may point to pip2 or pip3 depending on system
    pip3    # always pip for Python 3
    


  4. Python2 vs Python3 Key Language Differences

  5. Feature Python 2 Python 3
    print Statement: print "hi" Function: print("hi")
    Unicode str = bytes str = Unicode
    range Returns list Lazy object (efficient)
    Division 5 / 2 == 2 5 / 2 == 2.5
    Error handling except Exception, e except Exception as e
    Iterators Many return lists Return lightweight iterators
    f-strings ❌ Not available ✔ Available
    Type hints ❌ Not supported ✔ Fully supported


  6. Why Python 3 Was Introduced



  7. Python2 End-of-Life



  8. Should You Ever Use Python 2?



  9. Checking Your System’s Python Version

  10. python --version
    python3 --version
    

    import sys
    print(sys.version)
    


  11. Modern Recommendation (Most Important)

  12. python3
    pip3
    

    .venv/bin/python
    .venv/bin/pip
    


  13. Summary

  14. Concept Python Python 3
    Meaning today Usually Python 3, but may be Python 2 on old systems Always Python 3
    Status Python 2: EOL (dead) Actively maintained
    Unicode Broken model First-class Unicode
    Syntax Old syntax (print, exception syntax) Modern syntax: print(), exception chaining
    Tooling Very limited Type hints, virtualenvs, modern libraries
    Recommendation Do not use Use for all projects



pip / pip3 – Python Package Manager

  1. What Is pip?



  2. pip vs pip3

  3. python -m pip install requests
    python3 -m pip install requests
    



  4. Checking pip Installation and Version

  5. pip --version
    pip3 --version
    python -m pip --version
    



  6. Basic Installation Commands

  7. # install latest version of a package
    python -m pip install requests
    
    # install a specific version
    python -m pip install "requests==2.31.0"
    
    # install at least version X
    python -m pip install "requests>=2.31.0"
    
    # install from a local file
    python -m pip install ./my_package-0.1.0-py3-none-any.whl
    


  8. Upgrading and Uninstalling Packages

  9. # upgrade a package
    python -m pip install --upgrade requests
    
    # uninstall a package
    python -m pip uninstall requests
    



  10. Listing and Inspecting Installed Packages

  11. # list all installed packages
    python -m pip list
    
    # show detailed information about a package
    python -m pip show requests
    



  12. Using Requirements Files

  13. requests==2.31.0
    flask>=2.3
    numpy~=1.26
    

    python -m pip install -r requirements.txt
    

    python -m pip freeze > requirements.txt
    


  14. pip and Virtual Environments

  15. python -m venv .venv
    source .venv/bin/activate    # Linux/macOS
    # .venv\Scripts\activate     # Windows (PowerShell/CMD)
    
    python -m pip install requests
    


  16. Installing Your Own Project with pip

  17. # from project root directory
    python -m pip install .
    

    python -m pip install -e .
    



  18. Useful Options

  19. Option Meaning
    --upgrade Upgrade package to latest version
    --force-reinstall Reinstall even if version is already installed
    --no-deps Do not install dependencies automatically
    --user Install into user’s home directory (no root needed)
    --pre Allow pre-release and beta versions
    -r file.txt Install from requirements file


  20. Upgrading pip Itself

  21. python -m pip install --upgrade pip
    



  22. pip Cache and Offline Installs (Short Overview)

  23. python -m pip cache dir      # show cache location
    python -m pip cache list     # list cached packages
    



  24. Common Pitfalls

  25. Issue Explanation / Fix
    Using wrong pip Use python -m pip to ensure the correct interpreter
    Installing globally as root Better: use virtual environments or --user
    Conflicting versions Use separate venvs per project to isolate dependencies
    Missing packages in IDE Ensure IDE uses the same interpreter / venv where you installed packages


  26. Summary

  27. Concept Key Idea
    pip / pip3 Python’s package manager
    Best practice Use python -m pip inside a virtual environment
    Dependencies Manage via requirements.txt and pip freeze
    Installs install, uninstall, list, show
    Local projects pip install . and pip install -e .
    Safety Avoid global installs; prefer venvs



Python Packages

  1. What Is a Package?



  2. Basic Package Structure

  3. myproject/
        mypackage/
            __init__.py
            math_utils.py
            string_utils.py
        main.py
    



  4. Importing from Packages

  5. import mypackage
    

    from mypackage import math_utils
    

    from mypackage.math_utils import add, multiply
    

    import mypackage.string_utils as su
    


  6. The __init__.py File


  7. # mypackage/__init__.py
    
    from .math_utils import add
    from .string_utils import to_upper
    
    VERSION = "1.0.0"
    

    from mypackage import add, to_upper
    


  8. Package Search Path

  9. import sys
    print(sys.path)
    


  10. Subpackages

  11. mypackage/
        __init__.py
        data/
            __init__.py
            loader.py
        utils/
            __init__.py
            file.py
            debug.py
    

    from mypackage.data.loader import load_csv
    from mypackage.utils.debug import log
    


  12. Absolute vs Relative Imports

  13. from mypackage.utils.file import read_file
    

    from .utils.file import read_file           # same package
    from ..core.helpers import validate_input   # parent package
    



  14. Installing Your Package Locally

  15. python -m pip install .
    

    python -m pip install -e .
    



  16. Packaging for Distribution

  17. yourpackage/
        __init__.py
        module_a.py
        module_b.py
    pyproject.toml
    README.md
    LICENSE
    

    python -m build
    



  18. Namespace Packages (Advanced)

  19. analytics/
        models/
            user.py
        transformations/
            filter.py
    



  20. Summary

  21. Concept Meaning
    Module Single .py file
    Package Directory containing __init__.py and modules
    Subpackage Package inside another package
    Absolute import from package.module import x
    Relative import from .module import x
    Editable install pip install -e .
    Namespace package Package without __init__.py, can span multiple dirs



Python Virtual Environments

  1. What Is a Virtual Environment?



  2. Why Virtual Environments Are Needed



  3. Creating a Virtual Environment

  4. python -m venv .venv

    .venv/
        bin/ or Scripts/
        lib/
        pyvenv.cfg
    


  5. Activating a Virtual Environment

  6. source .venv/bin/activate

    .\.venv\Scripts\Activate.ps1

    (.venv) $



  7. Installing Packages Inside the Environment

  8. pip install requests

    pip list



  9. Deactivating

  10. deactivate



  11. Why python -m pip Is Better

  12. python -m pip install flask


  13. How Virtual Environments Work Internally

  14. pyvenv.cfg
    home = /usr/bin/python3.10
    include-system-site-packages = false
    



  15. Virtual Environment Best Practices

  16. .venv/

    python -m pip freeze > requirements.txt


  17. Virtual Environments vs Other Languages



  18. Alternatives to venv (Advanced Tools)



  19. Summary

  20. Concept Description
    Virtual environment Isolated Python interpreter + packages
    Purpose Avoid conflicts between project dependencies
    Creation python -m venv .venv
    Activation Use source .venv/bin/activate or Windows Activate.ps1
    Install packages Into the venv using python -m pip install
    Deactivation deactivate
    Comparison with other languages Python isolates entire interpreter, not just dependencies



Conda vs Anaconda

  1. Introduction



  2. What Is Conda?

  3. # create environment with a specific python version
    conda create -n myenv python=3.11
    
    # activate environment
    conda activate myenv
    
    # install packages
    conda install numpy


  4. What Is Anaconda?

  5. Anaconda = Python + Conda + Hundreds of pre-installed data-science packages


  6. What Is Miniconda?

  7. Miniconda = Python + Conda (minimal)


  8. Conda vs pip

  9. Feature pip conda
    Main purpose Install Python packages from PyPI Install any package (Python & non-Python)
    Manages Python versions? No Yes
    Environments via venv built-in environment manager
    Works offline? No Yes (with local channels)
    Speed Fast for pure Python packages Optimized for scientific stack
    Binary dependencies Hard to install Easy (bundled with C libraries)


  10. When Should You Use Conda?



  11. When Should You Use pip + venv?



  12. Summary: Conda vs Anaconda

  13. Concept Description
    Conda A package manager and environment manager.
    Anaconda A full scientific Python distribution that includes conda and hundreds of packages.
    Miniconda A minimal installer for conda without preinstalled packages.
    pip Python’s standard package manager; installs from PyPI only.
    Which one to choose? Conda for data science; pip+venv for general development.



Python Input

  1. Introduction



  2. Basic Usage of input()

  3. name = input("Enter your name: ")
    print("Hello,", name)
    x = input()
    


  4. input() Always Returns a String

  5. value = input("Enter a number: ")
    print(type(value))   # <class 'str'>
    

    age = int(input("Enter your age: "))
    pi  = float(input("Enter a float: "))
    


  6. Common Conversion Patterns

  7. n = int(input("n = "))
    
    x = float(input("x = "))
    
    flag = input("True/False? ").lower() == "true"
    
    a, b = input("Enter two numbers: ").split()
    a = int(a)
    b = int(b)
    
    nums = list(map(int, input("Numbers: ").split()))
    


  8. Using split() for Multi-Value Input

  9. text = "apple banana orange"
    items = text.split()
    # ["apple", "banana", "orange"]
    

    values = input("a b c: ").split()
    

    a, b, c = map(int, input().split())
    


  10. Stripping Input Automatically

  11. cmd = input("Command: ").strip()
    



  12. Error Handling for Invalid Input

  13. try:
        n = int(input("Enter number: "))
    except ValueError:
        print("Not a valid integer!")
    



  14. Reading Input Until a Condition

  15. while True:
        x = input("Enter int: ")
        if x.isdigit():
            x = int(x)
            break
        print("Try again.")
    


  16. Reading Multiple Lines (Advanced)

  17. lines = []
    while True:
        line = input()
        if line == "":
            break
        lines.append(line)
    

    import sys
    
    for line in sys.stdin:
        print("You entered:", line.strip())
    


  18. Advanced: ast.literal_eval for Safe Parsing

  19. import ast
    
    val = ast.literal_eval(input("Enter a list or number: "))
    print(type(val), val)
    



  20. Summary

  21. Feature Description
    input() Reads a line from the user, returns a string
    Always string You must convert to int/float manually
    Multiple values Use .split() and map()
    Error handling Use try/except when converting
    Reading many lines Use loops or sys.stdin
    Trimming whitespace .strip() removes leading/trailing spaces
    Safe literal parsing ast.literal_eval()



Python Output

  1. Introduction



  2. Basic Usage of print()

  3. print("Hello, world!")
    a = 10
    print(a)
    print(a + 5)
    



  4. Printing Multiple Values

  5. print("Sum:", 3 + 5)
    print("Coordinates:", x, y, z)
    



  6. Important Parameters: sep and end

  7. print("a", "b", "c", sep="-")
    # a-b-c
    

    print("Hello", end="...")
    print("World")
    # Output: Hello...World
    


  8. Printing Without Newline

  9. print("Loading", end="")
    print(".", end="")
    print(".", end="")
    print(".", end="")
    


  10. Formatted Output (f-strings)

  11. name = "Ammy"
    age = 25
    print(f"{name} is {age} years old.")
    

    print(f"2 + 2 = {2 + 2}")
    


  12. Advanced f-string Formatting

  13. pi = 3.1415926535
    print(f"{pi:.2f}")     # 3.14
    print(f"{pi:10.4f}")   # padded
    
    print(f"|{42:>5}|")    # right-aligned
    print(f"|{42:<5}|")    # left-aligned
    print(f"|{42:^5}|")    # centered
    


  14. Old-Style Formatting (Not Recommended)

  15. print("%s is %d years old" % ("Bob", 30))
    

    print("Name: {}, Age: {}".format("Bob", 30))
    



  16. Printing Objects

  17. class Person:
        def __init__(self, name):
            self.name = name
    
    p = Person("Ammy")
    print(p)   # default: <__main__.Person object at ...>
    

    class Person:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return f"Person({self.name})"
    
    p = Person("Ammy")
    print(p)
    


  18. Printing Debug Information

  19. value = 123
    print(f"{value = }")
    # value = 123
    



  20. Redirecting Output

  21. with open("output.txt", "w") as f:
        print("Hello file!", file=f)
    

    import sys
    print("Error!", file=sys.stderr)
    


  22. Printing Complex Data Structures

  23. users = [{"id": 1}, {"id": 2}]
    print(users)
    

    import pprint
    pprint.pprint(users)
    


  24. Flushing Output

  25. print("Processing...", flush=True)
    



  26. Printing Without Spaces Between Arguments

  27. print("A", "B", "C", sep="")
    # ABC
    


  28. Summary

  29. Feature Description
    print() Main function for output in Python
    sep Separator between items
    end What to print after line (default newline)
    f-strings Modern and recommended formatting style
    Redirecting output Use file= to print to files or stderr
    Pretty printing pprint prints complex nested structures cleanly
    flush Force immediate printing
    Debug printing Use {var = } in f-strings



Reading and Writing Files in Python

  1. Introduction



  2. The open() Function

  3. f = open("data.txt", "r")   # r = read mode
    


    Mode Meaning Description
    r Read File must exist
    w Write Overwrite or create new file
    a Append Add to the end of file
    r+ Read/Write File must exist
    b Binary Combine with other modes: rb, wb
    t Text Default mode


  4. Using with for Safe File Handling

  5. with open("data.txt", "r") as f:
        content = f.read()
    


  6. Reading Files

  7. with open("data.txt", "r") as f:
        text = f.read()
    

    with open("data.txt") as f:
        for line in f:
            print(line.strip())
    

    with open("data.txt") as f:
        lines = f.readlines()
    

    with open("data.txt") as f:
        partial = f.read(10)   # read first 10 chars
    


  8. Writing Files

  9. with open("output.txt", "w") as f:
        f.write("Hello world\n")
    

    with open("output.txt", "a") as f:
        f.write("Another line\n")
    

    lines = ["a\n", "b\n", "c\n"]
    
    with open("letters.txt", "w") as f:
        f.writelines(lines)
    


  10. Binary Files

  11. with open("image.png", "rb") as f:
        data = f.read()
    
    with open("copy.png", "wb") as f:
        f.write(data)
    


  12. File Paths

  13. open("subfolder/data.txt")
    open("/home/user/file.txt")
    open("C:\\Users\\John\\file.txt")
    

    open(r"C:\folder\file.txt")
    


  14. Checking File Existence

  15. from pathlib import Path
    
    p = Path("data.txt")
    if p.exists():
        print("File exists!")
    


  16. Error Handling for File Operations

  17. try:
        with open("no_such_file.txt") as f:
            data = f.read()
    except FileNotFoundError:
        print("File does not exist")
    

    except PermissionError:
        print("Permission denied")
    


  18. Reading and Writing JSON Files

  19. import json
    
    with open("config.json") as f:
        data = json.load(f)
    
    with open("output.json", "w") as f:
        json.dump(data, f, indent=2)
    


  20. Reading and Writing CSV Files

  21. import csv
    
    with open("data.csv") as f:
        reader = csv.reader(f)
        for row in reader:
            print(row)
    
    import csv
    
    rows = [["id", "name"], [1, "Ammy"]]
    with open("out.csv", "w", newline="") as f:
        writer = csv.writer(f)
        writer.writerows(rows)
    


  22. Summary

  23. Task Method
    Open a file open(path, mode)
    Safe open with open(...)
    Read all f.read()
    Read line-by-line Iterate file object
    Write to file f.write()
    Binary mode "rb", "wb"
    Modern paths pathlib.Path
    Error handling Use try/except



Errors and Exceptions in Python

  1. Introduction



  2. Two Main Types of Errors


  3. # Syntax error example
    if True
        print("hello")
    # Runtime exception example
    x = 10 / 0
    


  4. Common Built-in Exceptions

  5. Exception When It Happens
    SyntaxError Invalid Python code
    NameError Variable or name not found
    TypeError Wrong type used in an operation
    ValueError Correct type, but invalid value
    ZeroDivisionError Division by zero
    IndexError Out-of-bound list or tuple index
    KeyError Missing key in dictionary
    FileNotFoundError File does not exist
    PermissionError No permission to access resource
    ImportError Module cannot be imported
    AssertionError Assertion fails


  6. Handling Exceptions with try/except
  7. try:
        x = 10 / 0
    except ZeroDivisionError:
        print("Cannot divide by zero!")
    


  8. Handling Multiple Different Exceptions
  9. try:
        n = int(input("Enter a number: "))
        result = 10 / n
    except ValueError:
        print("You did not enter a valid integer.")
    except ZeroDivisionError:
        print("Cannot divide by zero.")
    


  10. Catching Multiple Exceptions in One Block
  11. try:
        x = int("abc")
    except (ValueError, TypeError):
        print("Something is wrong with the input.")
    


  12. Using else in Exception Handling

  13. try:
        f = open("data.txt")
    except FileNotFoundError:
        print("File not found.")
    else:
        print("File opened successfully!")
        f.close()
    


  14. The finally Block

  15. try:
        f = open("data.txt")
        data = f.read()
    except FileNotFoundError:
        print("Missing file.")
    finally:
        print("Closing file.")
        f.close()
    


  16. Raising Exceptions Yourself
  17. raise ValueError("Invalid data format")



  18. Creating Custom Exceptions
  19. class NegativeNumberError(Exception):
        pass
    
    def check(n):
        if n < 0:
            raise NegativeNumberError("No negative numbers allowed!")
    
    check(-5)
    



  20. Accessing Exception Details
  21. try:
        1 / 0
    except Exception as e:
        print(type(e))
        print(e)
    


  22. Ignoring Exceptions (Not Recommended)
  23. try:
        risky_action()
    except:
        pass
    


  24. Exception Hierarchy (Simplified)

  25. 
    BaseException
     ├── SystemExit
     ├── KeyboardInterrupt
     └── Exception
          ├── ValueError
          ├── TypeError
          ├── RuntimeError
          ├── OSError
          └── ...
    



  26. Summary

  27. Concept Description
    SyntaxError Code structure error caught before execution
    Exception Runtime error during program execution
    try/except Catch and handle exceptions
    else Runs only if no exception occurs
    finally Always runs (cleanup)
    raise Manually trigger exceptions
    Custom exceptions Define your own error types



Custom Exceptions in Python

  1. Why Define Custom Exceptions?



  2. Basic Custom Exception

  3. class MyError(Exception):
        pass
    
    def do_stuff():
        raise MyError("Something went wrong!")
    
    try:
        do_stuff()
    except MyError as e:
        print("Caught MyError:", e)
    



  4. Adding Custom Information

  5. class ConfigError(Exception):
        def __init__(self, key: str, message: str):
            self.key = key
            self.message = message
            super().__init__(f"[{key}] {message}")
    
    def load_config(key: str) -> str:
        # imagine it fails
        raise ConfigError(key, "Missing configuration value")
    
    try:
        load_config("DB_HOST")
    except ConfigError as e:
        print("Config error for key:", e.key)
        print("Details:", e.message)
    



  6. Exception Hierarchies for a Project

  7. class AppError(Exception):
        """Base class for all application-specific errors."""
        pass
    
    class DatabaseError(AppError):
        pass
    
    class NotFoundError(AppError):
        pass
    
    class PermissionDeniedError(AppError):
        pass
    

    try:
        ...
    except NotFoundError:
        print("Item not found, show 404")
    
    except AppError:
        print("Some other app-level error")


  8. Choosing Good Exception Names



  9. Wrapping Lower-Level Exceptions

  10. class StorageError(Exception):
        pass
    
    def read_user_file(path: str) -> str:
        try:
            with open(path) as f:
                return f.read()
        except OSError as e:
            # wrap OS error in a domain-specific one
            raise StorageError(f"Could not read {path}") from e
    



  11. Custom Exceptions and __str__ / __repr__

  12. class ApiError(Exception):
        def __init__(self, status: int, message: str):
            self.status = status
            self.message = message
            super().__init__(message)
    
        def __str__(self) -> str:
            return f"API error {self.status}: {self.message}"
    
    raise ApiError(404, "Resource not found")
    



  13. Custom Exceptions with Extra Context

  14. class ValidationError(Exception):
        def __init__(self, field: str, errors: list[str]):
            self.field = field
            self.errors = errors
            message = f"Validation failed for '{field}': {', '.join(errors)}"
            super().__init__(message)
    
    try:
        raise ValidationError("email", ["missing '@'", "too short"])
    except ValidationError as e:
        print(e.field)      # email
        print(e.errors)     # ["missing '@'", "too short"]
    



  15. Where to Put Custom Exceptions in a Project

  16. myapp/
        __init__.py
        exceptions.py   # all custom exception classes
        models.py
        services.py
    
    # exceptions.py
    class MyAppError(Exception):
        pass
    
    class AuthError(MyAppError):
        pass
    
    # services.py
    from .exceptions import AuthError
    
    def login(user, password):
        if not valid(user, password):
            raise AuthError("Invalid credentials")
    


  17. Best Practices



  18. Summary

  19. Aspect Recommendation
    Base class Inherit from Exception (or a library-specific base error)
    Naming End with Error, be descriptive (e.g. ConfigError)
    Hierarchy Create a root AppError / MyLibError and derive others
    Extra data Store extra fields in attributes; pass a friendly message to super().__init__
    Wrapping Use raise NewError(...) from e to keep original cause
    Location Large projects: put them in exceptions.py / errors.py



Python Classes

  1. Introduction



  2. Creating a Class
  3. class Person:
        pass
    


  4. Instantiating (Creating Objects)
  5. p = Person()
    print(p)


  6. The __init__ Method (Constructor)

  7. class Person:
        def __init__(self, name, age):
            self.name = name    # instance attribute
            self.age = age
    
    p = Person("Alice", 25)
    print(p.name)
    print(p.age)
    



  8. Instance Attributes

  9. class Point:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    p1 = Point(1, 2)
    p2 = Point(10, 20)
    
    print(p1.x, p2.x)
    


  10. Instance Methods

  11. class Circle:
        def __init__(self, radius):
            self.radius = radius
    
        def area(self):
            return 3.14 * self.radius * self.radius
    
    c = Circle(5)
    print(c.area())
    


  12. Class Attributes

  13. class Dog:
        species = "Canis lupus familiaris"   # class attribute
    
        def __init__(self, name):
            self.name = name     # instance attribute
    
    d1 = Dog("Buddy")
    d2 = Dog("Charlie")
    
    print(d1.species, d2.species, Dog.species)
    


  14. Methods vs Attributes (Key Concept)

  15. Type Stored Where? Shared?
    Instance Attributes Inside each object No
    Class Attributes On the class itself Yes
    Methods On the class (as functions) Shared by all instances


  16. Special Methods (Dunder Methods)

  17. class Person:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return f"Person({self.name})"
    
        def __repr__(self):
            return f"Person(name={self.name!r})"
    
    p = Person("Alice")
    print(p)        # calls __str__
    print([p])      # calls __repr__
    

  18. Other useful dunders:


  19. Class Methods

  20. class User:
        count = 0
    
        def __init__(self):
            User.count += 1
    
        @classmethod
        def how_many(cls):
            return cls.count
    
    print(User.how_many())


  21. Static Methods

  22. class Math:
        @staticmethod
        def add(a, b):
            return a + b
    
    print(Math.add(2, 3))
    


  23. Inheritance

  24. class Animal:
        def speak(self):
            return "..."
    
    class Dog(Animal):
        def speak(self):
            return "Woof!"
    
    d = Dog()
    print(d.speak())


  25. Calling the Parent Class
  26. class Person:
        def __init__(self, name):
            self.name = name
    
    class Employee(Person):
        def __init__(self, name, salary):
            super().__init__(name)    # calls Person.__init__
            self.salary = salary
    


  27. Multiple Inheritance

  28. class A: pass
    class B: pass
    class C(A, B): pass
    



  29. Encapsulation / Access Control

  30. self.name
    self._internal_value
    self.__secret



  31. Properties (Getters/Setters in Pythonic Way)
  32. class Person:
        def __init__(self, age):
            self._age = age
    
        @property
        def age(self):
            return self._age
    
        @age.setter
        def age(self, value):
            if value < 0:
                raise ValueError("Age cannot be negative")
            self._age = value
    
    
    
    p = Person(20)
    
    print(p.age)      # uses the @property -> calls age(self)
    # 20
    
    p.age = 25        # uses @age.setter -> calls age(self, 25)
    print(p.age)
    # 25
    
    p.age = -5        # also calls the setter
    # ValueError: Age cannot be negative
    



  33. Dataclasses (Python 3.7+ Convenience)
  34. from dataclasses import dataclass
    
    @dataclass
    class Point:
        x: int
        y: int
    
    p = Point(4, 5)
    print(p)
    



  35. Summary

  36. Concept Meaning
    Instance attribute Per-object data, stored on each instance
    Class attribute Shared across all instances
    Instance method Methods that operate on object state (self)
    Class method Receives cls; often constructors or class utilities
    Static method Namespace-only function inside class
    Dunder methods Customize behavior (printing, math, comparisons, etc.)
    Inheritance Reuse and extend behavior of other classes
    Dataclasses Auto-generated methods for simple data containers



Python Dataclasses

  1. Introduction



  2. Basic Dataclass

  3. from dataclasses import dataclass
    
    @dataclass
    class Point:
        x: int
        y: int
    
    p = Point(1, 2)
    print(p)          # Point(x=1, y=2)
    



  4. Type Annotations Are Required

  5. @dataclass
    class Item:
        name: str
        price: float
    


  6. Default Values
  7. @dataclass
    class User:
        name: str
        active: bool = True
    
    u = User("Alice")
    print(u.active)   # True
    


  8. Default Factory for Mutable Values

  9. from dataclasses import dataclass, field
    
    @dataclass
    class Bag:
        items: list[str] = field(default_factory=list)
    
    b1 = Bag()
    b2 = Bag()
    b1.items.append("apple")
    print(b2.items)   # []  (correct!)
    


  10. Immutability with frozen=True

  11. @dataclass(frozen=True)
    class Color:
        r: int
        g: int
        b: int
    
    c = Color(10, 20, 30)
    # c.r = 99  # error: cannot modify frozen dataclass
    



  12. Comparison Methods

  13. @dataclass(order=True)
    class Score:
        value: int
    
    print(Score(5) < Score(10))  # True
    

    @dataclass(eq=False)
    class Node:
        id: int
    


  14. Post-Initialization with __post_init__

  15. @dataclass
    class Product:
        name: str
        price: float
    
        def __post_init__(self):
            if self.price < 0:
                raise ValueError("Price cannot be negative")
    
    Product("Book", 20)    # OK
    Product("Free", -5)    # error!
    


  16. Excluding Fields from Dataclasses

  17. @dataclass
    class Cache:
        url: str
        data: bytes = field(repr=False)     # hide from repr
        expires: int = field(init=False)    # not passed to __init__
    
        def __post_init__(self):
            self.expires = 3600


  18. Make Dataclass Hashable

  19. @dataclass(frozen=True)
    class Coordinate:
        x: int
        y: int
    
    s = {Coordinate(1, 2)}
    



  20. Customizing Field Ordering

  21. @dataclass(order=True)
    class Item:
        sort_index: int = field(init=False, repr=False)
        name: str
        price: float
    
        def __post_init__(self):
            self.sort_index = self.price


  22. Inheritance with Dataclasses

  23. @dataclass
    class Animal:
        name: str
    
    @dataclass
    class Dog(Animal):
        breed: str
    
    d = Dog("Snoopy", "Beagle")
    print(d)
    



  24. Asdict and Astuple Conversion

  25. from dataclasses import asdict, astuple
    
    p = Point(3, 4)
    
    print(asdict(p))   # {'x': 3, 'y': 4}
    print(astuple(p))  # (3, 4)
    



  26. Dataclass vs. NamedTuple vs. TypedDict

  27. Feature Dataclass NamedTuple TypedDict
    Mutability Mutable (unless frozen) Immutable Mutable
    Methods Allows custom methods Methods possible, but inconvenient No methods
    Performance Good Very high Good
    Primary Use General data modeling Lightweight records JSON-like dict structures with types


  28. Summary

  29. Feature Description
    Automatic methods Creates __init__, __repr__, __eq__ automatically
    Default factory Safely create mutable defaults
    Frozen Immutability support (hashable)
    Order Enable comparison operators
    Post-init Custom setup via __post_init__
    Field customization Full control with field()
    asdict / astuple Convert to dict / tuple



Python Class Inheritance

  1. Introduction



  2. Basic Inheritance

  3. class Animal:
        def speak(self):
            return "..."
    
    class Dog(Animal):
        def speak(self):
            return "Woof!"
    
    d = Dog()
    print(d.speak())     # "Woof!"
    



  4. Using super() to Call Parent Methods

  5. class Person:
        def __init__(self, name):
            self.name = name
    
    class Employee(Person):
        def __init__(self, name, salary):
            super().__init__(name)      # calls Person.__init__
            self.salary = salary
    
    e = Employee("Alice", 5000)
    print(e.name, e.salary)
    



  6. Extending Methods Instead of Replacing
  7. class Logger:
        def log(self, msg):
            print("Log:", msg)
    
    class TimestampLogger(Logger):
        def log(self, msg):
            from datetime import datetime
            ts = datetime.now().isoformat()
            super().log(f"[{ts}] {msg}")
    


  8. Inheritance of Attributes and Methods

  9. class Vehicle:
        wheels = 4
    
    class Car(Vehicle):
        pass
    
    print(Car.wheels)   # 4
    


  10. Overriding Attributes
  11. class Vehicle:
        wheels = 4
    
    class Motorcycle(Vehicle):
        wheels = 2
    
    print(Motorcycle.wheels)   # 2
    


  12. Checking Inheritance Relationships
  13. issubclass(Dog, Animal)     # True
    isinstance(Dog(), Animal)   # True
    



  14. Multiple Inheritance

  15. class Flyer:
        def action(self):
            return "flying"
    
    class Swimmer:
        def action(self):
            return "swimming"
    
    class Duck(Flyer, Swimmer):
        pass
    
    d = Duck()
    print(d.action())     # "flying"  (Flyer is first)
    



  16. Method Resolution Order (MRO)

  17. print(Duck.mro())
    



  18. Using super() with Multiple Inheritance

  19. class A:
        def go(self):
            print("A.go")
    
    class B(A):
        def go(self):
            super().go()
            print("B.go")
    
    class C(A):
        def go(self):
            super().go()
            print("C.go")
    
    class D(B, C):
        pass
    
    D().go()
    
    Output:
    A.go
    C.go
    B.go
    



  20. Abstract Base Classes (ABC)

  21. from abc import ABC, abstractmethod
    
    class Shape(ABC):
        @abstractmethod
        def area(self):
            pass
    
    class Square(Shape):
        def __init__(self, side):
            self.side = side
    
        def area(self):
            return self.side * self.side
    



  22. Preventing Inheritance

  23. class FinalClass:
        def __init_subclass__(cls):
            raise TypeError("This class cannot be inherited from")
    


  24. Common Inheritance Patterns



  25. Summary

  26. Concept Description
    Inheritance Child class extends/overrides parent class
    super() Call parent method following MRO
    Multiple inheritance Class inherits from multiple parents
    MRO Defines attribute/method lookup order
    Overriding Child redefines parent method
    ABC Define abstract methods to enforce implementation
    Best practice Use inheritance for “is-a” relationships; use composition for “has-a”



Private Variables in Python

  1. Introduction



  2. Public Attributes

  3. class Person:
        name = "Alice"
    
    p = Person()
    print(p.name)   # accessible
    


  4. Protected Attributes (Single Underscore)

  5. class User:
        def __init__(self):
            self._password = "1234"   # internal
    
    u = User()
    print(u._password)   # works, but not recommended
    



  6. Private Attributes (Double Underscore)

  7. class BankAccount:
        def __init__(self, balance):
            self.__balance = balance
    
    acc = BankAccount(500)
    print(acc.__balance)     # AttributeError
    

    print(acc._BankAccount__balance)   # 500 (works but not intended!)
    



  8. Why Name-Mangling Exists

  9. class A:
        def __init__(self):
            self.__x = 1   # becomes _A__x
    
    class B(A):
        def __init__(self):
            super().__init__()
            self.__x = 2   # becomes _B__x (not overriding A's)
    



  10. Accessing Private Values via Methods

  11. class Counter:
        def __init__(self):
            self.__count = 0
    
        @property
        def count(self):
            return self.__count
    
        def increment(self):
            self.__count += 1
    


  12. Private Methods

  13. class Server:
        def __connect(self):
            print("Connecting...")
    
        def start(self):
            self.__connect()
    



  14. Private Attributes and Inheritance

  15. class Base:
        def __init__(self):
            self.__value = 10
    
    class Child(Base):
        def show(self):
            # print(self.__value)  # error
            print(self._Base__value)  # works
    



  16. When to Use What?

  17. Notation Meaning Use Case
    var Public Visible everywhere
    _var Protected by convention Internal detail, subclass-friendly
    __var Private (name-mangled) Avoid accidental overrides; “internal-only”


  18. Why Python Uses Conventions Instead of Real Privacy



  19. Summary

  20. Feature Description
    Public attributes Accessible everywhere
    Protected attributes Marked with _var, convention only
    Private attributes Double underscore; name-mangled
    Name mangling Prevents accidental name collisions in subclasses
    Private methods Same rules as private variables
    Properties Pythonic way to expose private fields safely



Python Iterators

  1. Introduction



  2. Everything Iterable in Python Produces an Iterator

  3. nums = [1, 2, 3]
    it = iter(nums)
    
    print(next(it))   # 1
    print(next(it))   # 2
    print(next(it))   # 3
    # next(it) would raise StopIteration
    


  4. The Iterator Protocol

  5. class CountTo:
        def __init__(self, limit):
            self.limit = limit
            self.current = 0
    
        def __iter__(self):
            return self
    
        def __next__(self):
            if self.current >= self.limit:
                raise StopIteration
            value = self.current
            self.current += 1
            return value
    
    for n in CountTo(3):
        print(n)
    
    Output:
    0
    1
    2
    


  6. Iterators Are State Machines

  7. it = iter([10, 20, 30])
    for i in it:
        print(i)
    
    for i in it:
        print(i)   # prints nothing (iterator exhausted)
    


  8. Iterators vs Iterables

  9. Concept Description
    Iterable Object that can return an iterator with iter()
    Iterator Object that produces values via next() and remembers state
    Iterator Protocol Requires __iter__() and __next__()


  10. Using Iterators in For-Loops

  11. for ch in "abc":
        print(ch)
    


  12. Files Are Iterators

  13. with open("data.txt") as f:
        for line in f:
            print(line.strip())
    


  14. Generators: The Most Common Way to Build Iterators
  15. def countdown(n):
        while n > 0:
            yield n
            n -= 1
    
    for x in countdown(3):
        print(x)
    


  16. Generator Expressions
  17. squares = (x*x for x in range(5))
    print(next(squares))   # 0
    print(next(squares))   # 1
    


  18. Itertools: Tools for Advanced Iteration
  19. import itertools
    
    for x in itertools.count(5, 2):
        print(x)
        if x > 12:
            break
    


  20. Iterator Exhaustion

  21. lst = [1, 2, 3]
    it = iter(lst)
    
    for i in it:
        print(i)
    
    for i in it:
        print(i)   # nothing printed
    


  22. Reiterable Objects

  23. nums = [1, 2, 3]
    for x in nums:
        print(x)
    
    for x in nums:
        print(x)   # works again
    


  24. Building Your Own Iterable + Iterator Pair

  25. class Reverse:
        def __init__(self, data):
            self.data = data
    
        def __iter__(self):
            return ReverseIterator(self.data)
    
    class ReverseIterator:
        def __init__(self, data):
            self.data = data
            self.index = len(data)
    
        def __next__(self):
            if self.index == 0:
                raise StopIteration
            self.index -= 1
            return self.data[self.index]
    
    words = Reverse("abc")
    for w in words:
        print(w)
    


  26. Summary

  27. Concept Description
    Iterator Object with __iter__ and __next__
    Iterable Object that produces an iterator via iter()
    StopIteration Signals end of iteration
    Generators Simplest way to create iterators
    Iterator exhaustion Iterators cannot be reused; create another
    Itertools Advanced iterator utilities



Python Generators

  1. Introduction



  2. Basic Generator Function

  3. def countdown(n):
        while n > 0:
            yield n
            n -= 1
    
    for x in countdown(3):
        print(x)
    
    Output:
    3
    2
    1
    



  4. Generator Execution Model

  5. g = countdown(3)
    print(next(g))   # 3
    print(next(g))   # 2
    print(next(g))   # 1
    # next(g) -> StopIteration
    


  6. Difference Between Return and Yield

  7. Keyword Effect
    return Ends function, returns a value once
    yield Pauses execution, returns multiple values one at a time


  8. Generators Are Memory-Efficient

  9. nums = [x*x for x in range(1_000_000)]
    

    nums = (x*x for x in range(1_000_000))
    


  10. Generator Expressions

  11. squares = (n*n for n in range(5))
    print(next(squares))   # 0
    print(next(squares))   # 1
    


  12. Generators with Multiple Yields

  13. def steps():
        yield "start"
        yield "loading"
        yield "finish"
    


  14. Using Generators to Build Pipelines

  15. def read_numbers():
        for i in range(10):
            yield i
    
    def even_numbers(numbers):
        for n in numbers:
            if n % 2 == 0:
                yield n
    
    def squared(numbers):
        for n in numbers:
            yield n * n
    
    pipeline = squared(even_numbers(read_numbers()))
    for x in pipeline:
        print(x)
    



  16. Sending Data into a Generator


  17. def greeter():
        name = yield "Your name?"
        yield f"Hello, {name}!"
    
    g = greeter()
    print(next(g))           # "Your name?"
    print(g.send("Alice"))   # "Hello, Alice!"
    



  18. How send() Works Internally

  19. name = yield "Your name?"
    



  20. The First send() Must Be None

  21. g = greeter()
    g.send("Alice")  # ERROR! Generator not started yet
    

    g = greeter()
    next(g)           # or g.send(None)
    g.send("Alice")
    


  22. Using Generators as Simple Coroutines

  23. def accumulator():
        total = 0
        while True:
            value = yield total
            total += value
    
    acc = accumulator()
    print(next(acc))        # 0
    print(acc.send(5))      # 5
    print(acc.send(10))     # 15
    print(acc.send(3))      # 18
    



  24. Stopping a Coroutine with a Sentinel Value

  25. def summer():
        total = 0
        while True:
            item = yield total
            if item is None:      # sentinel value
                return total
            total += item
    
    g = summer()
    next(g)
    print(g.send(5))   # 5
    print(g.send(7))   # 12
    
    try:
        g.send(None)   # stops the generator
    except StopIteration as e:
        print("Final total:", e.value)
    



  26. Sending Exceptions into a Generator

  27. def worker():
        try:
            while True:
                task = yield
                print("Processing", task)
        except ValueError:
            print("ValueError inside generator!")
        yield "done"
    
    g = worker()
    next(g)
    g.send("Job 1")
    g.throw(ValueError)
    print(next(g))   # "done"
    



  28. Difference Between next() and send()

  29. Method Purpose Value Passed to Generator
    next(gen) Resume generator None
    gen.send(value) Resume generator and pass value value


  30. Practical Use Cases



  31. Closing a Generator
  32. g = countdown(3)
    g.close()
    


  33. Throwing Exceptions into Generators

  34. def test():
        try:
            yield 1
        except ValueError:
            yield "error handled"
        yield 2
    
    g = test()
    print(next(g))              # 1
    print(g.throw(ValueError))  # "error handled"
    print(next(g))              # 2
    


  35. Returning from a Generator

  36. def total():
        s = 0
        while True:
            n = yield
            if n is None:
                break
            s += n
        return s
    


  37. Infinite Generators

  38. def naturals():
        n = 0
        while True:
            yield n
            n += 1
    


  39. Generators vs Regular Functions
  40. Regular Function Generator
    Returns once Yields many times
    Does not preserve state Resumes from last yield
    Eager evaluation Lazy evaluation
    Returns ordinary values Returns an iterator


  41. Summary

  42. Feature Description
    yield Produces values lazily
    Stateful execution Generator pauses and resumes
    Memory efficient Only one value stored at a time
    Generator expressions Lightweight inline generators
    send() / throw() Two-way communication with generator
    Pipelines Ideal for streaming data processing



Brief Tour of the Python Standard Library

  1. Introduction



  2. Operating System Interfaces: os and sys

  3. import os, sys
    
    print(os.getcwd())          # current working directory
    print(os.listdir("."))      # list files
    print(sys.version)          # python version
    print(sys.argv)             # command-line arguments
    


  4. File and Path Handling: pathlib

  5. from pathlib import Path
    
    p = Path("notes.txt")
    print(p.exists())
    print(p.read_text())
    


  6. Mathematics: math, statistics, random

  7. import math, statistics, random
    
    print(math.sqrt(9))
    print(statistics.mean([1,2,3,4]))
    print(random.randint(1, 10))
    


  8. Dates and Times: datetime

  9. from datetime import datetime, timedelta
    
    now = datetime.now()
    print(now)
    print(now + timedelta(days=3))
    


  10. Collections: collections

  11. from collections import Counter, defaultdict, deque
    
    c = Counter("banana")
    print(c)                      # character frequencies
    
    d = defaultdict(int)
    d["a"] += 1                   # no KeyError
    
    queue = deque([1,2,3])
    queue.appendleft(0)
    print(queue)
    


  12. Working with JSON: json

  13. import json
    
    data = {"name": "Alice", "age": 25}
    text = json.dumps(data)
    print(text)
    
    loaded = json.loads(text)
    print(loaded)
    


  14. Regular Expressions: re

  15. import re
    
    pattern = r"\d+"
    print(re.findall(pattern, "The year is 2025"))
    


  16. Working with the Internet: urllib.request, http.client

  17. from urllib import request
    
    with request.urlopen("https://example.com") as f:
        html = f.read()
        print(len(html))
    


  18. Command-Line Argument Parsing: argparse

  19. import argparse
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--name")
    args = parser.parse_args()
    
    print(f"Hello {args.name}")
    


  20. Data Compression: gzip, zipfile

  21. import gzip
    
    with gzip.open("data.gz", "wt") as f:
        f.write("Hello")
    


  22. Debugging and Profiling: pdb, timeit

  23. import timeit
    
    print(timeit.timeit("sum(range(1000))", number=1000))
    


  24. Temporary Files: tempfile

  25. import tempfile
    
    with tempfile.TemporaryFile() as f:
        f.write(b"Hello")
        f.seek(0)
        print(f.read())
    


  26. Binary Data: struct

  27. import struct
    
    packed = struct.pack("!I", 12345)
    unpacked = struct.unpack("!I", packed)
    print(unpacked)
    


  28. File Formats: csv, xml.etree.ElementTree

  29. import csv
    
    with open("data.csv") as f:
        reader = csv.reader(f)
        for row in reader:
            print(row)
    


  30. Threading and Concurrency: threading, multiprocessing

  31. import threading
    
    def worker():
        print("working")
    
    t = threading.Thread(target=worker)
    t.start()
    t.join()
    


  32. Popular General-Purpose Modules

  33. Module Purpose
    shutil File operations (copy, move, delete)
    subprocess Run external commands
    logging Configurable logging system
    functools Higher-order functions, caching
    itertools Fast iterator tools (infinite sequences, combinations)
    hashlib Cryptographic hashes (MD5, SHA256)


  34. Summary

  35. Category Important Modules
    System interaction os, sys, shutil
    Paths & Files pathlib, csv, tempfile
    Data formats json, xml, csv
    Math & Stats math, statistics, random
    Networking urllib, http.client
    Utilities timeit, functools, itertools
    Compression gzip, zipfile
    Concurrency threading, multiprocessing



Python Wrappers

  1. What Is a Wrapper in Python?



  2. Basic Wrapper Structure

  3. def wrapper(func):
        def inner():
            print("Before")
            result = func()
            print("After")
            return result
        return inner
    
    def say_hello():
        print("Hello")
    
    wrapped = wrapper(say_hello)
    wrapped()
    

    Before
    Hello
    After
    


  4. Why Do We Need a Wrapper?



  5. Wrappers With Arguments

  6. def wrapper(func):
        def inner(*args, **kwargs):
            print("Calling:", func.__name__)
            return func(*args, **kwargs)
        return inner
    
    def add(a, b):
        return a + b
    
    wrapped_add = wrapper(add)
    print(wrapped_add(3, 5))
    


  7. Preserving Function Metadata

  8. import functools
    
    def wrapper(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            return func(*args, **kwargs)
        return inner
    


  9. Wrapper Example: Logging Function Calls

  10. import functools
    
    def log_calls(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            print(f"Calling {func.__name__} with {args=} {kwargs=}")
            result = func(*args, **kwargs)
            print(f"{func.__name__} returned {result}")
            return result
        return inner
    
    @log_calls
    def multiply(x, y):
        return x * y
    
    multiply(3, 4)
    


  11. Wrapper Example: Timing Functions

  12. import time
    import functools
    
    def timer(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            end = time.perf_counter()
            print(f"{func.__name__} took {end - start:.6f} seconds")
            return result
        return inner
    
    @timer
    def slow():
        time.sleep(1)
    
    slow()
    


  13. Wrapper Example: Checking Permissions

  14. def require_admin(func):
        @functools.wraps(func)
        def inner(user, *args, **kwargs):
            if user != "admin":
                raise PermissionError("Access denied")
            return func(user, *args, **kwargs)
        return inner
    
    @require_admin
    def delete_database(user):
        return "Database deleted!"
    
    delete_database("admin")    # OK
    delete_database("guest")    # PermissionError
    


  15. Wrapper Example: Caching (Memoization)

  16. def cache(func):
        stored = {}
    
        @functools.wraps(func)
        def inner(x):
            if x not in stored:
                stored[x] = func(x)
            return stored[x]
        return inner
    
    @cache
    def square(n):
        print("Computing...")
        return n * n
    
    print(square(5))   # computed
    print(square(5))   # from cache
    


  17. Wrappers vs Decorators

  18. 
    Without decorator:
        wrapped = wrapper(func)
    
    With decorator:
        @wrapper
        def func(): ...
    



  19. Using Wrappers With Methods (Classes)

  20. def debug(func):
        @functools.wraps(func)
        def inner(self, *args, **kwargs):
            print(f"Method {func.__name__} called")
            return func(self, *args, **kwargs)
        return inner
    
    class A:
        @debug
        def hello(self):
            print("Hello from A")
    
    A().hello()
    


  21. Advanced: Wrapping Async Functions

  22. import functools
    import asyncio
    
    def async_wrapper(func):
        @functools.wraps(func)
        async def inner(*args, **kwargs):
            print("Before async call")
            result = await func(*args, **kwargs)
            print("After async call")
            return result
        return inner
    
    @async_wrapper
    async def hello():
        await asyncio.sleep(1)
        return "Done"
    
    asyncio.run(hello())
    


  23. Summary

  24. Concept Description
    Wrapper A function that adds behavior around another function
    *args / **kwargs Allow wrappers to handle any function signature
    functools.wraps Preserves function metadata (name, docstring)
    Without decorator wrapped = wrapper(func)
    With decorator @wrapper above the function
    Common uses logging, timing, caching, permissions, validation, retries



Python Decorators

  1. What Is a Decorator?

  2. @decorator
    def function():
        ...
    
    function = decorator(function)


  3. Why Decorators Are Important



  4. How Decorators Work (Fundamentals)

  5. def my_decorator(func):
        def wrapper():
            print("Before call")
            func()
            print("After call")
        return wrapper
    
    @my_decorator
    def hello():
        print("Hello!")
    
    hello()
    

    Before call
    Hello!
    After call
    


  6. Decorators With Arguments

  7. def decorator(func):
        def wrapper(*args, **kwargs):
            print("Calling:", func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    @decorator
    def add(a, b):
        return a + b
    
    print(add(3, 5))
    


  8. Preserving Metadata Using functools.wraps

  9. import functools
    
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    



  10. Decorators With Parameters

  11. def repeat(n):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                for _ in range(n):
                    func(*args, **kwargs)
            return wrapper
        return decorator
    
    @repeat(3)
    def hello():
        print("Hello")
    
    hello()
    



  12. Class-Based Decorators

  13. class Decorator:
        def __init__(self, func):
            self.func = func
    
        def __call__(self, *args, **kwargs):
            print("Before")
            value = self.func(*args, **kwargs)
            print("After")
            return value
    
    @Decorator
    def greet():
        print("Hi!")
    
    greet()
    


  14. Decorators for Methods

  15. def log_method(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            print(f"Method {func.__name__} called")
            return func(self, *args, **kwargs)
        return wrapper
    
    class A:
        @log_method
        def hello(self):
            print("Hello")
    


  16. Decorating Class Methods (classmethod, staticmethod)

  17. def debug(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print("Debug:", func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    class C:
        @debug
        @classmethod
        def foo(cls):
            print("Class method")
    


  18. Real-World Decorator Examples

  19. 1. Logging
  20. def log(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{func.__name__} called")
            return func(*args, **kwargs)
        return wrapper
    
    @log
    def work():
        print("Working...")
    


  21. 2. Timing
  22. import time
    
    def timer(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            value = func(*args, **kwargs)
            end = time.perf_counter()
            print(f"{func.__name__} took {end-start:.5f}s")
            return value
        return wrapper
    


  23. 3. Memoization / Caching
  24. def cache(func):
        stored = {}
        @functools.wraps(func)
        def wrapper(x):
            if x not in stored:
                stored[x] = func(x)
            return stored[x]
        return wrapper
    


  25. 4. Authorization (permissions)
  26. def require_admin(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            if user != "admin":
                raise PermissionError("Not allowed")
            return func(user, *args, **kwargs)
        return wrapper
    


  27. 5. Retry Decorator
  28. import time
    
    def retry(n):
        def decorator(func):
            @functools.wraps(func)
            def wrapper(*args, **kwargs):
                for i in range(n):
                    try:
                        return func(*args, **kwargs)
                    except Exception as e:
                        print(f"Retry {i+1}/{n}: {e}")
                        time.sleep(1)
                raise RuntimeError("Failed after retries")
            return wrapper
        return decorator
    


  29. Decorators for Asynchronous Functions

  30. def async_decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            print("Before await")
            result = await func(*args, **kwargs)
            print("After await")
            return result
        return wrapper
    


  31. Stacking Multiple Decorators

  32. @log
    @timer
    def compute():
        return sum(range(1_000_000))
    

    
    compute = log(timer(compute))
    


  33. Decorators on Classes

  34. def make_all_methods_debugged(cls):
        for name, value in cls.__dict__.items():
            if callable(value):
                setattr(cls, name, debug(value))
        return cls
    
    @make_all_methods_debugged
    class A:
        def hello(self): print("Hello")
        def world(self): print("World")
    


  35. Common Built-In Decorators

  36. Decorator Purpose
    @staticmethod Method that has no self
    @classmethod Method that receives cls instead of self
    @property Turn methods into computed attributes
    @functools.lru_cache Built-in caching decorator


  37. Summary

  38. Concept Meaning
    Decorator Function that wraps another function to add behavior
    Wrapper Inner function that surrounds the original function
    @decorator Syntax sugar for func = decorator(func)
    functools.wraps Keeps metadata (function name, docstring)
    Decorator arguments Requires 3-layer structure: decorator(args) → decorator → wrapper
    Common uses logging, caching, timing, authentication, retries
    Advanced class decorators, async decorators, stacked decorators



Introduction to Python Async Programming

  1. What Is Asynchronous Programming in Python?



  2. Key Terminology



  3. How Async Differs From Sync Code

  4. Sync Code Async Code
    Runs line by line Pauses during slow I/O and resumes later
    Blocks until the operation finishes Yields control while waiting
    Great for CPU-heavy tasks Great for I/O-heavy tasks


  5. Basic Syntax: async and await

  6. import asyncio
    
    async def greet():
        await asyncio.sleep(1)
        print("Hello async!")
    
    asyncio.run(greet())
    


  7. Async Functions Return Coroutines

  8. async def add(a, b):
        return a + b
    
    result = add(1, 2)
    print(result)    # <coroutine object ...>
    
    print(asyncio.run(add(1, 2)))    # 3
    


  9. Running Multiple Tasks Concurrently

  10. async def say(name):
        await asyncio.sleep(1)
        print(name)
    
    async def main():
        await asyncio.gather(
            say("A"),
            say("B"),
            say("C"),
        )
    
    asyncio.run(main())
    


  11. Turning Coroutines into Background Tasks

  12. async def worker():
        await asyncio.sleep(2)
        print("done!")
    
    async def main():
        task = asyncio.create_task(worker())
        print("task started...")
        await task
    
    asyncio.run(main())
    


  13. Async I/O (Network Example)

  14. import asyncio
    import aiohttp   # third-party async HTTP client
    
    async def fetch(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as resp:
                return await resp.text()
    
    async def main():
        content = await fetch("https://example.com")
        print(content[:50])
    
    asyncio.run(main())
    


  15. Async Context Managers & Iterators

  16. async with session.get(url) as resp:
        ...
    
    async for msg in websocket:
        print(msg)
    


  17. Where Async Shines



  18. Where Async Fails



  19. Combining Async With Django

  20. async def async_view(request):
        await asyncio.sleep(1)
        return HttpResponse("Async response")
    



Understanding the Difference Between asyncio.run() and await in Python

  1. Big Picture: What Problem Are We Discussing?



  2. What asyncio.run() Actually Does?



  3. What await Actually Does?

  4. import asyncio
    
    async def compute():
        print("Start compute")
        await asyncio.sleep(1)
        print("End compute")
        return 10
    
    async def main():
        result = await compute()  # await another coroutine
        print("Result:", result)
    
    asyncio.run(main())
    


  5. Key Difference: Where You Can Use Them

  6. # OK – top-level script
    if __name__ == "__main__":
        asyncio.run(main())
    
    # This is NOT allowed:
    result = await compute()  # SyntaxError outside async def
    


  7. Example: Using Both Together Correctly
  8. import asyncio
    
    async def fetch_data():
        print("Fetching...")
        await asyncio.sleep(1)
        print("Done!")
        return {"data": 123}
    
    async def main():
        # We are already inside async world
        result = await fetch_data()
        print("Result from fetch:", result)
    
    # Bridge from sync world to async world
    if __name__ == "__main__":
        asyncio.run(main())
    


  9. You Cannot Use asyncio.run() Inside a Running Event Loop

  10. async def inner():
        # WRONG: this will raise
        asyncio.run(other_coroutine())  # RuntimeError: asyncio.run() cannot be called from a running event loop
    
    async def inner():
        # CORRECT:
        await other_coroutine()
    


  11. Summary in one phrase: asyncio.run(coro) blcoks, but await yields.



Introduction to the Python C API

  1. What Is the Python C API?



  2. Basic Structure of a C Extension Module

  3. #include <Python.h>
    
    static PyObject * add(PyObject * self, PyObject * args) {
        int a, b;
        if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
            return NULL;
        }
    
        return PyLong_FromLong(a + b);
    }
    
    static PyMethodDef MyMethods[] = {
        {"add", add, METH_VARARGS, "Add two numbers"},
        {NULL, NULL, 0, NULL}
    };
    
    static struct PyModuleDef mymodule = {
        PyModuleDef_HEAD_INIT,
        "mymodule",
        NULL,
        -1,
        MyMethods
    };
    
    PyMODINIT_FUNC PyInit_mymodule(void) {
        return PyModule_Create(&mymodule);
    }
    
    import mymodule
    print(mymodule.add(3, 4))   # 7
    


  4. The Core Python Object Type: PyObject

  5. typedef struct _object {
        Py_ssize_t ob_refcnt;   // reference count
        PyTypeObject *ob_type;  // pointer to its type
    } PyObject;
    


  6. Reference Counting (IMPORTANT!)

  7. Py_INCREF(obj);   // increase reference count
    Py_DECREF(obj);   // decrease, free if goes to 0
    


  8. Converting Between Python and C Types

  9. Python C API creation C API extraction
    int PyLong_FromLong PyLong_AsLong
    str PyUnicode_FromString PyUnicode_AsUTF8
    list PyList_New PyList_GetItem
    dict PyDict_New PyDict_GetItem

    int a, b;
    PyArg_ParseTuple(args, "ii", &a, &b);
    


  10. Raising Python Exceptions from C

  11. PyErr_SetString(PyExc_ValueError, "Invalid value");
    return NULL;
    


  12. Embedding Python into a C Program

  13. #include <Python.h>
    
    int main() {
        Py_Initialize();
        PyRun_SimpleString("print('Hello from embedded Python!')");
        Py_Finalize();
        return 0;
    }
    


  14. Working with Python Modules in C

  15. PyObject * mod    = PyImport_ImportModule("math");
    PyObject * func   = PyObject_GetAttrString(mod, "sqrt");
    PyObject * result = PyObject_CallFunction(func, "d", 9.0);
    


  16. Defining New Python Types in C

  17. typedef struct {
        PyObject_HEAD
        double x;
    } Point;
    


  18. The GIL (Global Interpreter Lock)

  19. Py_BEGIN_ALLOW_THREADS
        // long CPU computation here
    Py_END_ALLOW_THREADS
    


  20. Building a C Extension Module

  21. from setuptools import setup, Extension
    
    setup(
        name="mymodule",
        ext_modules=[
            Extension("mymodule", ["mymodule.c"])
        ]
    )
    
    python3 setup.py build
    python3 setup.py install
    



Calling C Code from Python

  1. Why Call C from Python?



  2. Overview of Approaches

  3. Method Requires compiling? Good for Difficulty
    C extension (Python C API) Yes (.so / .pyd) Deep integration, custom types Hardest, but most powerful
    ctypes Uses existing .so / .dll Quick bindings to shared libs Medium
    cffi Often yes; can also use ABI mode Nice C-like declarations in Python Medium
    Cython Yes, compiles .pyx to C Mixed Python + C performance Medium


  4. Calling C via a Python C Extension Module

  5. /* mymodule.c */
    #include <Python.h>
    
    static PyObject * my_add(PyObject * self, PyObject * args) {
        int a, b;
        if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
            return NULL;  /* raises TypeError automatically */
        }
        int result = a + b;
        return PyLong_FromLong(result);
    }
    
    /* Method table */
    static PyMethodDef MyMethods[] = {
        {"add", my_add, METH_VARARGS, "Add two integers"},
        {NULL, NULL, 0, NULL}
    };
    
    /* Module definition */
    static struct PyModuleDef mymodule = {
        PyModuleDef_HEAD_INIT,
        "mymodule",       /* module name */
        "Example module", /* optional docstring */
        -1,               /* size of per-interpreter state, or -1 */
        MyMethods
    };
    
    /* Initialization function */
    PyMODINIT_FUNC PyInit_mymodule(void) {
        return PyModule_Create(&mymodule);
    }
    
    
    # setup.py
    from setuptools import setup, Extension
    
    module = Extension(
        "mymodule",
        sources=["mymodule.c"],
    )
    
    setup(
        name="mymodule",
        version="0.1",
        ext_modules=[module],
    )
    
    python3 setup.py build
    python3 setup.py install
    
    import mymodule
    
    print(mymodule.add(3, 5))   # 8
    


  6. Calling C with ctypes (Standard Library)

  7. /* mymath.c */
    int add(int a, int b) {
        return a + b;
    }
    
    import ctypes
    from ctypes import c_int
    
    # Load the shared library
    lib = ctypes.CDLL("./libmymath.so")
    
    # Declare argument and return types
    lib.add.argtypes = (c_int, c_int)
    lib.add.restype = c_int
    
    result = lib.add(2, 5)
    print(result)    # 7
    


  8. Passing Pointers and Arrays with ctypes

  9. /* sum_array.c */
    int sum_array(const int* data, int n) {
        int s = 0;
        for (int i = 0; i < n; ++i) {
            s += data[i];
        }
        return s;
    }
    
    import ctypes
    from ctypes import c_int
    
    lib = ctypes.CDLL("./libsumarray.so")
    lib.sum_array.argtypes = (ctypes.POINTER(c_int), c_int)
    lib.sum_array.restype = c_int
    
    # Create C array from Python list
    arr = (c_int * 4)(1, 2, 3, 4)
    result = lib.sum_array(arr, 4)
    print(result)   # 10
    


  10. Calling C with cffi (C Foreign Function Interface)

  11. from cffi import FFI
    
    ffi = FFI()
    ffi.cdef("int add(int a, int b);")
    
    lib = ffi.dlopen("./libmymath.so")
    
    result = lib.add(2, 3)
    print(result)   # 5
    


  12. Using Cython as a “Friendly Frontend” to C

  13. /* cmathlib.h */
    int add(int a, int b);
    
    /* cmathlib.c */
    int add(int a, int b) {
        return a + b;
    }
    
    # mymodule.pyx
    cdef extern from "cmathlib.h":
        int add(int a, int b)
    
    def py_add(a: int, b: int) -> int:
        return add(a, b)
    
    import mymodule
    print(mymodule.py_add(3, 4))  # 7
    


  14. Which Method Should I Use?

  15. Scenario Recommended Method Reason
    You already have a compiled C library (.so / .dll) ctypes or cffi No need to touch the C code; write bindings in Python.
    You want deep integration with Python objects C extension (Python C API) Maximum control, can define new Python types.
    You want speed but prefer Python-like syntax Cython Gradual optimization, simpler than raw C API.
    You want fine control of ABI/API with clean C syntax cffi C-like declarations with a nice interface.



Python Threading

  1. What Is Threading in Python?



  2. Importing the Threading Module
  3. import threading
    


  4. Creating and Starting a Thread

  5. import threading
    import time
    
    def task():
        print("Task started")
        time.sleep(2)
        print("Task finished")
    
    t = threading.Thread(target=task)
    t.start()          # start the thread
    t.join()           # wait for the thread to finish
    


  6. Passing Arguments to a Thread
  7. def greet(name):
        print(f"Hello, {name}")
    
    t = threading.Thread(target=greet, args=("Junzhe",))
    t.start()
    


  8. Creating a Thread by Subclassing Thread
  9. class Worker(threading.Thread):
        def run(self):
            print("Worker running")
    
    w = Worker()
    w.start()
    


  10. Daemon Threads

  11. def background():
        while True:
            print("Running...")
            time.sleep(1)
    
    t = threading.Thread(target=background, daemon=True)
    t.start()
    


  12. The Global Interpreter Lock (GIL)



  13. Thread Synchronization with Lock



  14. Other Synchronization Primitives



  15. Queue: The Best Way to Pass Data Between Threads



  16. Comparison: Threading vs Multiprocessing vs Asyncio