Skip to content

Python

Points to consider

  • dynamically typed language means the type of a variable is determined at runtime, not when the code is written.

Python Data Structures: Operations & Methods

Operation String List Tuple Dict
Create s = "hello" πŸ’‘ l = [1,2,3] ✏️ t = (1,2,3) πŸ’‘ d = {"a":1,"b":2} ✏️
Read s[0] πŸ’‘ l[0] ✏️ t[0] πŸ’‘ d["a"] ✏️
Update ❌ Immutable πŸ’‘ l[0] = 10 ✏️ ❌ Immutable πŸ’‘ d["a"] = 10 ✏️
Delete ❌ πŸ’‘ del l[0] ✏️ ❌ πŸ’‘ del d["a"] ✏️
Length len(s) πŸ’‘ len(l) πŸ’‘ len(t) πŸ’‘ len(d) πŸ’‘
Search "he" in s πŸ’‘ 2 in l πŸ’‘ 2 in t πŸ’‘ "a" in d πŸ’‘
Substring / Subsequence s[1:4] πŸ’‘ l[1:4] πŸ’‘ t[1:4] πŸ’‘ ❌ πŸ’‘
Sort sorted(s) πŸ’‘ l.sort() ✏️ / sorted(l) πŸ’‘ sorted(t) πŸ’‘ sorted(d) πŸ’‘
Slice s[1:3] πŸ’‘ l[1:3] πŸ’‘ t[1:3] πŸ’‘ ❌ πŸ’‘
Reverse s[::-1] πŸ’‘ l.reverse() ✏️ / l[::-1] πŸ’‘ t[::-1] πŸ’‘ ❌ πŸ’‘
Methods s.upper(), s.lower(), s.split(), s.replace("a","b") πŸ’‘ l.append(4), l.extend([5,6]), l.pop(), l.remove(2) ✏️ t.count(1), t.index(2) πŸ’‘ d.keys(), d.values(), d.items(), d.get("a") πŸ’‘

Legend

  • πŸ’‘ Immutable / Returns New Object – The original object is not modified.
  • ✏️ Mutable / In-place Modification – The original object changes.

🐍 Python Loops & Conditional Statements Cheat Sheet

Statement Type Syntax / Example Notes
If statement if condition:
      # code
Executes block if condition is True
If-Else statement if condition:
      # code
else:
      # code
Executes else block if condition is False
If-Elif-Else statement if condition1:
      # code
elif condition2:
      # code
else:
      # code
Handles multiple conditions
Ternary / Conditional Expression number = 5
x = 10 if number > 0 else 20
print(x)
Single-line conditional assignment
For loop for i in iterable:
      # code
Iterates over sequence, list, string, tuple, dict keys, etc.
For loop with range
for i in range(5):
      # code
Generates numbers 0–4
While loop
while condition:
      # code
Loops while condition is True
Break statement
for i in range(5):
      if i == 3:
           break
Exits the nearest enclosing loop
Continue statement
for i in range(5):
      if i == 2:
           continue
      print(i)
Skips current iteration
Pass statement
if condition:
      pass
Placeholder; does nothing

πŸ—‚οΈ File & Folder CRUD Operations

Operation File Folder
Create open("file.txt", "w") / open("file.txt", "x") os.mkdir("folder")
os.makedirs("folder/subfolder")
Read open("file.txt", "r").read()
readline() / readlines()
os.listdir("folder")
Update / Write open("file.txt", "a").write("text")
open("file.txt", "w").write("overwrite")
❌ Direct update not applicable (use files inside folder)
Delete os.remove("file.txt") os.rmdir("folder") (empty)
shutil.rmtree("folder") (non-empty)
Check existence os.path.exists("file.txt") os.path.exists("folder")
Rename / Move os.rename("old.txt", "new.txt") os.rename("old_folder", "new_folder")
shutil.move("folder", "new_path")

Exception

1. Exception Handling Structure

Python uses try, except, else, and finally blocks.

try:
    # Code that may raise an exception
    result = 10 / 0
except ZeroDivisionError as e:
    # Handle specific exception
    print("Error: Division by zero!", e)
except Exception as e:
    # Handle any other exception
    print("Some error occurred:", e)
else:
    # Executes if no exception occurs
    print("Result is", result)
finally:
    # Executes no matter what
    print("Execution finished")

Key points:

try β†’ risky code

except β†’ handles exceptions

else β†’ runs if no exception

finally β†’ runs always (cleanup)

2. Throwing (Raising) an Exception

#Use raise to throw exceptions.

x = -5
if x < 0:
    raise ValueError("x must be non-negative")

#Throws a ValueError with a custom message.

3. User-Defined Exception

You can create custom exceptions by inheriting from Exception:

##### Define custom exception
class MyCustomError(Exception):
    def __init__(self, message):
        super().__init__(message)

# Raise the custom exception
age = -1
if age < 0:
    raise MyCustomError("Age cannot be negative!")

#Inherits from Exception (or a more specific subclass if desired).

#Can add custom attributes or methods for richer error info.


Exception hierarchy

The class hierarchy for built-in exceptions is:

# Python Exception Hierarchy: Catchable vs Non-Catchable

BaseException
β”œβ”€β”€ ❌ BaseExceptionGroup
β”œβ”€β”€ ❌ GeneratorExit
β”œβ”€β”€ ❌ KeyboardInterrupt
β”œβ”€β”€ ❌ SystemExit
└── βœ… Exception
     β”œβ”€β”€ βœ… ArithmeticError
     β”‚    β”œβ”€β”€ βœ… FloatingPointError
     β”‚    β”œβ”€β”€ βœ… OverflowError
     β”‚    └── βœ… ZeroDivisionError
     β”œβ”€β”€ βœ… AssertionError
     β”œβ”€β”€ βœ… AttributeError
     β”œβ”€β”€ βœ… BufferError
     β”œβ”€β”€ βœ… EOFError
     β”œβ”€β”€ βœ… ExceptionGroup
     β”œβ”€β”€ βœ… ImportError
     β”‚    └── βœ… ModuleNotFoundError
     β”œβ”€β”€ βœ… LookupError
     β”‚    β”œβ”€β”€ βœ… IndexError
     β”‚    └── βœ… KeyError
     β”œβ”€β”€ βœ… MemoryError
     β”œβ”€β”€ βœ… NameError
     β”‚    └── βœ… UnboundLocalError
     β”œβ”€β”€ βœ… OSError
     β”‚    β”œβ”€β”€ βœ… BlockingIOError
     β”‚    β”œβ”€β”€ βœ… ChildProcessError
     β”‚    β”œβ”€β”€ βœ… ConnectionError
     β”‚    β”‚    β”œβ”€β”€ βœ… BrokenPipeError
     β”‚    β”‚    β”œβ”€β”€ βœ… ConnectionAbortedError
     β”‚    β”‚    β”œβ”€β”€ βœ… ConnectionRefusedError
     β”‚    β”‚    └── βœ… ConnectionResetError
     β”‚    β”œβ”€β”€ βœ… FileExistsError
     β”‚    β”œβ”€β”€ βœ… FileNotFoundError
     β”‚    β”œβ”€β”€ βœ… InterruptedError
     β”‚    β”œβ”€β”€ βœ… IsADirectoryError
     β”‚    β”œβ”€β”€ βœ… NotADirectoryError
     β”‚    β”œβ”€β”€ βœ… PermissionError
     β”‚    β”œβ”€β”€ βœ… ProcessLookupError
     β”‚    └── βœ… TimeoutError
     β”œβ”€β”€ βœ… ReferenceError
     β”œβ”€β”€ βœ… RuntimeError
     β”‚    β”œβ”€β”€ βœ… NotImplementedError
     β”‚    β”œβ”€β”€ βœ… PythonFinalizationError
     β”‚    └── βœ… RecursionError
     β”œβ”€β”€ βœ… StopAsyncIteration
     β”œβ”€β”€ βœ… StopIteration
     β”œβ”€β”€ βœ… SyntaxError
     β”‚    └── βœ… IndentationError
     β”‚         └── βœ… TabError
     β”œβ”€β”€ βœ… SystemError
     β”œβ”€β”€ βœ… TypeError
     β”œβ”€β”€ βœ… ValueError
     β”‚    └── βœ… UnicodeError
     β”‚         β”œβ”€β”€ βœ… UnicodeDecodeError
     β”‚         β”œβ”€β”€ βœ… UnicodeEncodeError
     β”‚         └── βœ… UnicodeTranslateError
     └── βœ… Warning
          β”œβ”€β”€ βœ… BytesWarning
          β”œβ”€β”€ βœ… DeprecationWarning
          β”œβ”€β”€ βœ… EncodingWarning
          β”œβ”€β”€ βœ… FutureWarning
          β”œβ”€β”€ βœ… ImportWarning
          β”œβ”€β”€ βœ… PendingDeprecationWarning
          β”œβ”€β”€ βœ… ResourceWarning
          β”œβ”€β”€ βœ… RuntimeWarning
          β”œβ”€β”€ βœ… SyntaxWarning
          β”œβ”€β”€ βœ… UnicodeWarning
          └── βœ… UserWarning

βœ… Legend:

  • βœ… Catchable in try-except (subclasses of Exception)

  • ❌ Non-catchable / System exit exceptions (directly under BaseException)

βœ… Rule of thumb:

  • Catch Exception and its subclasses.

  • Avoid catching BaseException directly unless you specifically want to handle system exit or interrupts (rare).

Threads

Non-Daemon Thread Case:

The main thread waits for all non-daemon threads to finish before the program exits.

By default, all threads are non-daemon threads i

Daemon Thread Case:

The main thread does not wait for daemon threads to complete.

When the main thread finishes executing, the program exits.

All running daemon threads are automatically terminated at that time.

βœ… In short:

Non-daemon thread: Program waits for completion.

Daemon thread: Program exits without waiting; daemon threads stop immediately.

Synchronizing Threads

simple thread

import threading  # Import threading module to work with threads

def helloworld():
    print("Hello World!")  # Function that prints a message

t1 = threading.Thread(target=helloworld)  # Create a thread that will execute helloworld()
t1.start()  # Start the thread and run the function concurrently

thread join

import threading  # Import threading module to create and manage threads

def hello():
    for x in range(50):  # Loop 50 times
        print("Hello!")  # Print Hello each time

t1 = threading.Thread(target=hello)  # Create a thread that will run the hello() function
t1.start()  # Start the thread execution

t1.join()  # Wait for thread t1 to finish before continuing

print("Another Text")  # This will print only after the thread finishes

Lock and Release

import threading
import time

x = 8192  # Shared variable that both threads will modify

lock = threading.Lock()  # Lock to ensure only one thread accesses x at a time

def double():
    global x, lock  # Use the shared variable and lock
    lock.acquire()  # Acquire the lock before entering critical section

    while x < 16384:  # Keep doubling until x reaches 16384
        x *= 2  # Double the value of x
        print(x)  # Print the updated value
        time.sleep(1)  # Pause for 1 second to simulate processing

    print("Reached the maximum!")  # Message when maximum limit is reached
    lock.release()  # Release the lock so other threads can use the resource

def halve():
    global x, lock  # Use the shared variable and lock
    lock.acquire()  # Acquire the lock before modifying x

    while x > 1:  # Keep halving until x becomes 1
        x /= 2  # Divide the value of x by 2
        print(x)  # Print the updated value
        time.sleep(1)  # Pause for 1 second

    print("Reached the minimum!")  # Message when minimum limit is reached
    # Note: lock.release() is missing here, so the lock may remain held

t1 = threading.Thread(target=halve)   # Thread that halves the value
t2 = threading.Thread(target=double)  # Thread that doubles the value

t1.start()  # Start the halve thread
t2.start()  # Start the double thread

semaphore

import threading
import time

semaphore = threading.BoundedSemaphore(value=5)  # Allow maximum 5 threads at the same time

def access(thread_number):
    print("{} is trying to access!".format(thread_number))  # Thread requests access
    semaphore.acquire()  # Acquire semaphore (wait if limit reached)

    print("{} was granted access!".format(thread_number))  # Access granted
    time.sleep(5)  # Simulate using the shared resource for 5 seconds

    print("{} is now releasing!".format(thread_number))  # Thread finished its work
    semaphore.release()  # Release semaphore so another thread can access

for thread_number in range(1, 11):  # Create 10 threads
    t = threading.Thread(target=access, args=(thread_number,))  # Create thread
    t.start()  # Start thread execution
    time.sleep(1)  # Start each thread with 1-second gap

Events

import threading  # Import threading module

event = threading.Event()  # Create an Event object used for thread synchronization

def myfunction():
    print("Waiting for event to trigger...\n")  # Message showing the thread is waiting
    event.wait()  # Block the thread until the event is triggered
    print("Performing action XYZ now...")  # Execute action after event is set

t1 = threading.Thread(target=myfunction)  # Create a thread to run myfunction
t1.start()  # Start the thread

x = input("Do you want to trigger the event? (y/n)")  # Ask user whether to trigger the event
if x == "y":
    event.set()  # Trigger the event and wake up the waiting thread

Queue

The queue module in Python provides three thread-safe queue types: FIFO Queue, LIFO Queue, and Priority Queue.

FIFO

A standard queue.Queue in Python follows the FIFO (First-In, First-Out) principle. You can visualize it like a line at a grocery store: the first person to get in line is the first person to be served.

import queue

# Initialize a new First-In-First-Out (FIFO) queue object
q = queue.Queue()

# Define a list of integers to be processed
numbers = [10, 20, 30, 40, 50, 60, 70]

# Iterate through the list and add each integer to the back of the queue
for number in numbers:
    q.put(number)

# Remove and return the first item that was put into the queue (10)
print(q.get())

LIFO

This script demonstrates a Last-In, First-Out behavior. Think of it like a stack of trays; the last one you put on top is the first one you pick up.

import queue

# Create a Last-In, First-Out (LIFO) queue, essentially a Stack
q = queue.LifoQueue()

# Define a list of numbers from 1 to 7
numbers = [1, 2, 3, 4, 5, 6, 7]

# Loop through the list and push each number onto the top of the stack
for x in numbers:
    q.put(x)

# Since it is LIFO, get() removes and returns the most recently added item (7)
print(q.get())

Priority Queue

This script uses a Priority Queue, where items are retrieved based on their assigned priority value rather than the order they were added. In Python, the lowest value is given the highest priority.

import queue

# Create a Priority Queue
q = queue.PriorityQueue()

# Adding tuples: (priority_number, data)
# Note: Lower numbers are processed first
q.put((2, "Hello World!"))
q.put((11, 99))
q.put((5, 7.5))
q.put((1, True)) # This has the lowest value (1), so it will be first

# Continue looping as long as the queue is not empty
while not q.empty():
    # Removes and prints items in ascending order of priority
    print(q.get())

Socket

Basic Socket Server

This snippet sets up a simple TCP server that listens for incoming connections on the local machine and sends a welcome message to any client that connects.

import socket

# Create a new socket using IPv4 (AF_INET) and TCP (SOCK_STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to the local address 127.0.0.1 and port 55555
s.bind(('127.0.0.1', 55555))

# Put the server in listening mode to wait for clients
s.listen()

# Infinite loop to keep the server running and accepting connections
while True:
    # Block and wait for a new connection; returns the client socket and address
    client, address = s.accept()

    # Print the address of the newly connected client to the console
    print("Connected to {}".format(address))

    # Send an encoded welcome message to the client
    client.send("You are connected!".encode())

    # Close the connection with the current client
    client.close()

Basic Socket Client

Python script for a simple TCP client

import socket  # Import the built-in library for network communication

# Create a socket object using IPv4 (AF_INET) and TCP (SOCK_STREAM)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Connect to the server running on the local machine (127.0.0.1) at port 55555
s.connect(('127.0.0.1', 55555))

# Receive up to 1024 bytes of data from the server
message = s.recv(1024)

# Close the connection to free up the resource
s.close()

# Convert the received bytes into a readable string and print it
print(message.decode())

DB

SQLlite

Basic workflow for using the sqlite3 library in Python: connecting to a database, creating a table, inserting data, and querying that data.

import sqlite3

# Connect to the database file (creates it if it doesn't exist)
connection = sqlite3.connect('mydata.db')

# Create a cursor object to execute SQL commands
cursor = connection.cursor()

# Create a new table named 'persons' with three columns
cursor.execute("""
CREATE TABLE IF NOT EXISTS persons (
    first_name TEXT,
    last_name TEXT,
    age INTEGER
)
""")

# Insert three records into the 'persons' table
cursor.execute("""
INSERT INTO persons VALUES
('Paul', 'Smith', 24),
('Mark', 'Johnson', 42),
('Anna', 'Smith', 34)
""")

# Query the database for all columns where the last name is 'Smith'
cursor.execute("""
SELECT * FROM persons
WHERE last_name = 'Smith'
""")

# Retrieve all results from the query and print them as a list of tuples
rows = cursor.fetchall()
print(rows)

# Save (commit) the changes to the database
connection.commit()

# Close the connection to the database file
connection.close()

Recursion

Search file in a dir and its sub dir

import os

def find_item(path, name):
    # Get all items (files + directories) in the current path
    items = os.listdir(path)

    # First pass: check if the target exists directly in this directory
    for item in items:
        # Construct the full absolute path of the item
        full_path = os.path.join(path, item)

        # If the name matches the target file/folder name
        if item == name:
            # Return the full path immediately (stop searching)
            return full_path

    # Second pass: recursively search inside subdirectories
    for item in items:
        # Construct the full path again
        full_path = os.path.join(path, item)

        # If the item is a directory, search inside it
        if os.path.isdir(full_path):
            # Recursive call to search inside this directory
            result = find_item(full_path, name)

            # If the item was found in the recursive search
            if result:
                # Return the found path immediately
                return result

    # If the item was not found in this directory or any subdirectory
    return None


# Example usage
result = find_item("/your/start/path", "target_name")
print(result)

XML Processing

Comparison of packages

Feature minidom SAX ElementTree lxml
Read XML βœ… βœ… βœ… βœ…
Write XML βœ… ❌ βœ… βœ…
Modify (add/delete nodes) βœ… ❌ βœ… βœ…
XPath search ❌ ❌ Limited βœ…
Schema validation (XSD) ❌ ❌ ❌ βœ…
Speed Slow Very fast Fast Very fast
Memory usage High Very low Moderate Moderate
Large XML support Poor Excellent Good Excellent
---

SAX (Simple API for XML)

SAX (Simple API for XML) is designed as an event-driven parser. It "streams" through an XML document from top to bottom, triggering "events" (like startElement or endElement) as it goes. Because it doesn't load the whole file into memory, it is incredibly fast and efficient for reading massive files.

import xml.sax

# Define a custom handler to manage XML events
class GroupHandler(xml.sax.ContentHandler):
    def __init__(self):
        # Initialize attributes to avoid AttributeErrors if a tag is missing
        self.current = ""
        self.name = ""
        self.age = ""
        self.weight = ""
        self.height = ""

    def startElement(self, name, attrs):
        """Called when an opening tag like <person id="123"> is found."""
        self.current = name
        if self.current == "person":
            print("\n----- PERSON -----")
            # Extract the 'id' attribute from the person tag
            if 'id' in attrs:
                print("ID: {}".format(attrs['id']))

    def characters(self, content):
        """Called when text data inside a tag is processed."""
        if self.current == "name":
            self.name = content
        elif self.current == "age":
            self.age = content
        elif self.current == "weight":
            self.weight = content
        elif self.current == "height":
            self.height = content

    def endElement(self, name):
        """Called when a closing tag like </name> is found."""
        if name == "name":
            print("Name: {}".format(self.name))
        elif name == "age":
            print("Age: {}".format(self.age))
        elif name == "weight":
            # Fixed typo: changed 'self.seig' from image to 'self.weight'
            print("Weight: {}".format(self.weight))
        elif name == "height":
            # Fixed logic: changed second 'age' check from image to 'height'
            print("Height: {}".format(self.height))

        # Reset current tag to prevent processing whitespace between tags
        self.current = ""

# --- Execution Logic ---

# 1. Create the handler instance
handler = GroupHandler()

# 2. Initialize the SAX parser
parser = xml.sax.make_parser()

# 3. Connect the handler to the parser
parser.setContentHandler(handler)

# 4. Feed the XML file into the parser
# Ensure 'data.xml' exists in your directory!
try:
    parser.parse('data.xml')
except FileNotFoundError:
    print("Error: 'data.xml' not found. Please ensure the file exists.")

Minimal DOM implementation

This code uses the xml.dom.minidom library in Python to read, display, and modify an XML file

import xml.dom.minidom

# Load and parse the XML file into a DOM tree
domtree = xml.dom.minidom.parse('data.xml')
# Get the root element of the document (the 'group' tag)
group = domtree.documentElement

# Extract all elements with the tag name 'person'
persons = group.getElementsByTagName('person')

# Iterate through each person and print their details
for person in persons:
    print("-----PERSON-----")
    # Check for and print the 'id' attribute if it exists
    if person.hasAttribute('id'):
        print("ID: {}".format(person.getAttribute('id')))

    # Access child nodes to extract text data for Name, Age, Weight, and Height
    print("Name: {}".format(person.getElementsByTagName('name')[0].childNodes[0].data))
    print("Age: {}".format(person.getElementsByTagName('age')[0].childNodes[0].data))
    print("Weight: {}".format(person.getElementsByTagName('weight')[0].childNodes[0].data))
    print("Height: {}".format(person.getElementsByTagName('height')[0].childNodes[0].data))

# Create a new 'person' element and set its ID attribute
newperson = domtree.createElement('person')
newperson.setAttribute('id', '6')

# Create the 'name' element and add text to it
name = domtree.createElement('name')
name.appendChild(domtree.createTextNode('Paul Green'))

# Create the 'age' element and add text to it
age = domtree.createElement('age')
age.appendChild(domtree.createTextNode('19'))

# Create the 'weight' element and add text to it
weight = domtree.createElement('weight')
weight.appendChild(domtree.createTextNode('80'))

# Create the 'height' element and add text to it
height = domtree.createElement('height')
height.appendChild(domtree.createTextNode('179'))

# Append all new sub-elements to the new 'person' container
newperson.appendChild(name)
newperson.appendChild(age)
newperson.appendChild(weight)
newperson.appendChild(height)

# Append the new person to the root 'group' element
group.appendChild(newperson)

# Save the updated DOM tree back to the 'data.xml' file
domtree.writexml(open('data.xml', 'w'))

data.xml

<group>
    <person id="1">
        <name>Mike Smith</name>
        <age>34</age>
        <weight>90</weight>
        <height>175</height>
    </person>
    <person id="2">
        <name>Anna Smith</name>
        <age>54</age>
        <weight>91</weight>
        <height>188</height>
    </person>
    <person id="3">
        <name>Bob Johnson</name>
        <age>25</age>
        <weight>76</weight>
        <height>190</height>
    </person>
    <person id="4">
        <name>Sara Jones</name>
        <age>56</age>
        <weight>82</weight>
        <height>170</height>
    </person>
</group>

Logging

import logging

# Configures the root logger to output to the console at the DEBUG level
logging.basicConfig(level=logging.DEBUG)

# Creates a custom logger instance named "MyLogger"
logger = logging.getLogger("MyLogger")
# Sets the internal threshold for this logger to capture all messages (DEBUG and above)
logger.setLevel(logging.DEBUG)

# Creates a handler that writes log messages to a file named "mylog.log"
handler = logging.FileHandler("mylog.log")
# Sets the file handler to only record messages that are INFO or higher (ignoring DEBUG)
handler.setLevel(logging.INFO)

# Defines the visual format of the log: "LEVEL - TIMESTAMP: MESSAGE"
formatter = logging.Formatter("%(levelname)s - %(asctime)s: %(message)s")
# Applies the defined format to the file handler
handler.setFormatter(formatter)

# Attaches the file handler to our custom logger
logger.addHandler(handler)

# This will appear in the console (due to basicConfig) but NOT in the file (due to handler level)
logger.debug("This is a debug message!")
# This will appear in both the console and the "mylog.log" file
logger.info("This is important information!")

Magic / Dunder(Double Underscore) methods

class Vector:

    # The constructor: Initializes new instances of the class
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # Operator Overloading: Defines behavior for the '+' operator
    def __add__(self, other):
        # Returns a new Vector object with the sums of x and y
        return Vector(self.x + other.x, self.y + other.y)

    # String Representation: Defines how the object looks when printed or inspected
    def __repr__(self):
        return f"X: {self.x}, Y: {self.y}"

    # Length: Defines what len(obj) returns (hardcoded to 10 here)
    def __len__(self):
        return 10

    # Callable: Allows the object to be called like a function using ()
    def __call__(self):
        print("Called")

# --- Execution ---

v1 = Vector(10, 20)
v2 = Vector(50, 60)

# Triggers __add__: v1 is 'self', v2 is 'other'
v3 = v1 + v2

# Triggers __len__: prints 10
print(len(v3))

# Accessing the attribute directly: prints 60 (10 + 50)
print(v3.x)

# Triggers __call__: prints "Called"
v1()

Decorators

A decorator is a function that modifies the behavior of another function or method without changing its code.

In other words, it’s a wrapper around a function.

Decorators are commonly used for logging, authentication, timing, caching, and more.

#Simple example

def decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

@decorator
def say_hello():
    print("Hello!")

say_hello()

##############################
#Another Example

def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)  # Call the original function with arguments
        print("After function call")
        return result  # Return the original function's result
    return wrapper

@decorator
def say_hello(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# Usage
message = say_hello("John", greeting="Hi")
print("Returned message:", message)

Generators

def infinite_sequence():

  result = 1

  yield result

  result *= 5

  yield result

  result *= 5

  yield result

  result *= 5



values = infinite_sequence()
print(type(values))

for x in values:
  print(x)

Argument Parsing

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--name")
args = parser.parse_args()

print(args.name)

Encapsulation

Encapsulation is the mechanism of bundling data and methods together and restricting direct access to the internal state of an object.

class Person:

    surname="Smith"

    def __init__(self, name):
        self.__name=name

    def __PersonPersonal(self):
        print("__PersonPersonal__")

    @property
    def Name(self):
        return self.__name

    @Name.setter
    def Name(self, name):
        self.__name=name

    def printName(self):
        print(self.__name)
        print(Person.surname)

    @staticmethod
    def sprintName():
        print(Person.surname)


    @staticmethod
    def setsprintName(name):
        Person.surname=name

    def callPrivateMethod(self):
        self.__PersonPersonal()

class Employee(Person):

    def callPrivateMethod(self):
        self.__PersonPersonal()

p = Person("john")
print(p._Person__name)


p.Name=20
print(p.Name)


p.printName()

p.sprintName()

p.setsprintName("adam")

p.sprintName()

p.callPrivateMethod()

print("\n##############\n")
e = Employee("Jamie")
e.sprintName()
e.setsprintName("Bran")
e.sprintName()
e.callPrivateMethod();

Type Hinting

# Python Type Hinting Cheat Sheet

# -------------------------------
# Basic Variable Type Hints
# -------------------------------

age: int = 25                 # integer
price: float = 10.5           # float
name: str = "John"            # string
is_active: bool = True        # boolean
value: None = None            # None type


# -------------------------------
# Function Type Hints
# -------------------------------

def add(a: int, b: int) -> int:      # function takes two ints and returns int
    return a + b

def print_name(name: str) -> None:   # returns nothing
    print(name)


# -------------------------------
# Collection Types (Python 3.9+)
# -------------------------------

numbers: list[int] = [1, 2, 3]                     # list of integers
names: list[str] = ["John", "Jane"]                # list of strings

user_ages: dict[str, int] = {"John": 30}           # dictionary (key:str, value:int)

point: tuple[int, int] = (10, 20)                  # tuple of two integers

unique_items: set[str] = {"a", "b", "c"}           # set of strings


# -------------------------------
# Multiple Possible Types (Union)
# -------------------------------

value: int | str = "hello"     # value can be int OR string

# older syntax
from typing import Union
value2: Union[int, str] = 10


# -------------------------------
# Optional Type
# -------------------------------

name2: str | None = None       # variable can be string or None

# older syntax
from typing import Optional
name3: Optional[str] = None


# -------------------------------
# Function Returning Multiple Values
# -------------------------------

def get_user() -> tuple[str, int]:   # returns (name, age)
    return ("John", 30)


# -------------------------------
# List of Objects
# -------------------------------

class Person:
    pass

people: list[Person] = []       # list containing Person objects


# -------------------------------
# Any Type (disable type checking)
# -------------------------------

from typing import Any

data: Any = "hello"             # can hold any type
data = 10
data = [1, 2, 3]


# -------------------------------
# Function as Parameter
# -------------------------------

from typing import Callable

def operate(func: Callable[[int, int], int]) -> int:
    return func(2, 3)

def multiply(a: int, b: int) -> int:
    return a * b

result = operate(multiply)


# -------------------------------
# Type Alias
# -------------------------------

UserId = int          # creating alias for int

user_id: UserId = 10


# -------------------------------
# Generic Type
# -------------------------------

from typing import TypeVar

T = TypeVar("T")      # generic type

def identity(value: T) -> T:    # returns same type it receives
    return value

identity(10)
identity("hello")


# -------------------------------
# Class with Type Hints
# -------------------------------

class Employee:

    def __init__(self, name: str, age: int):
        self.name: str = name
        self.age: int = age

    def greet(self) -> str:
        return f"Hello {self.name}"


# -------------------------------
# Older Typing Imports (Python <3.9)
# -------------------------------

from typing import List, Dict, Tuple, Set

numbers_old: List[int] = [1,2,3]
user_old: Dict[str, int] = {"age":30}
point_old: Tuple[int, int] = (10,20)
items_old: Set[str] = {"a","b"}


# -------------------------------
# Important Notes
# -------------------------------

# 1. Type hints DO NOT enforce types at runtime
# 2. They are mainly used for:
#    - IDE autocomplete
#    - static type checking
#    - better readability
#
# 3. Tools used for checking types:
#    - mypy
#    - pyright
#    - pylint
#
# Example: Python will still run this without error
x: int = "hello"     # incorrect type but Python will not stop execution

Functions

function definition

default parameters

keyword arguments

*args

**kwargs

lambda functions

Lambda in Python is a small anonymous one-line function used to perform simple operations without defining a full function using def.

Example Code Explanation
Basic Lambda square = lambda x: x * x Creates a small function to return the square of a number.
Multiple Arguments add = lambda a, b: a + b Returns the sum of two numbers.
Immediate Execution (lambda x: x * 2)(5) Defines and runs a lambda function instantly.
With map() list(map(lambda x: x * 2, [1,2,3])) Applies a function to every element in the list.
With filter() list(filter(lambda x: x % 2 == 0, [1,2,3,4])) Filters elements that satisfy the condition.
With sorted() sorted(data, key=lambda x: x["age"]) Sorts items based on a specific key value.
Convert Data Type list(map(lambda x: int(x), ["10","20"])) Converts string numbers to integers.
Conditional Lambda lambda x: "Even" if x % 2 == 0 else "Odd" Returns a value based on a condition.
Max Value lambda a, b: a if a > b else b Returns the larger of two numbers.
String Transformation list(map(lambda x: x.upper(), names)) Converts each string in a list to uppercase.
Extract Field list(map(lambda x: x["name"], users)) Extracts a specific field from dictionaries.
Check Condition list(filter(lambda x: x > 10, numbers)) Filters numbers greater than 10.

Quick Syntax

lambda arguments: expression

recursion vs iteration

Comprehensions

Comprehensions in Python provide a concise way to create lists, sets, or dictionaries dynamically using logic instead of manually hard-coding the values.

### List comprehension  
squares = [x*x for x in range(10)]  

### Dict comprehension  
d = {x: x*x for x in range(5)}  

### Set comprehension  
unique = {x for x in numbers}

### Generator comprehension  
gen = (x*x for x in range(10))

Context Managers (with)

Context managers (with statement) are used to set up a resource, use it, and then clean it up reliably, even if an exception occurs

#Example:

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

Context managers are not limited to files. They are broadly used for:

  • Resource management – files, sockets, databases.

    import sqlite3
    with sqlite3.connect('example.db') as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM table")
    # connection is automatically closed
    
    ###########################
    
    import socket
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.connect(('example.com', 80))
        s.sendall(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')
    # socket automatically closed
    

  • Thread/multiprocessing locks – safe concurrency.

    import threading
    lock = threading.Lock()
    with lock:
        # critical section
        do_something()
    # lock automatically released
    

  • Temporary state changes – directories, I/O redirection.

    import sys
    from contextlib import redirect_stdout
    
    with open('output.txt', 'w') as f:
        with redirect_stdout(f):
            print("This goes into output.txt")
    

  • Transactions – commit/rollback logic.

    with conn:
        conn.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
    # commits if no exception, rolls back if exception
    

  • Custom reusable setups and teardowns – any code needing guaranteed cleanup.

    #Custom context manager:
    
    from contextlib import contextmanager
    
    @contextmanager
    def mycontext():
        print("enter")
        yield
        print("exit")
    

Iterators & Iterable Protocol

You show generators, but not the iterator protocol.

#Example:

class Counter:
    def __iter__(self):
        return self

    def __next__(self):
        ...

"""Explain:

iterable

iterator

__iter__

__next__

StopIteration"""

collections Module

from collections import Counter from collections import defaultdict from collections import deque from collections import namedtuple

dataclasses

enum

from enum import Enum

class Status(Enum):
    SUCCESS = 1
    FAIL = 2

functools

Useful decorators and utilities.

Examples:

from functools import lru_cache

Memoization:

@lru_cache def fib(n): ...

Other utilities:

partial

reduce

wraps

itertools

Important for iteration patterns.

Example:

import itertools

for x in itertools.permutations([1,2,3]): print(x)

Common ones:

count

cycle

repeat

permutations

combinations

chain

Async Programming

Example:

import asyncio

async def main(): print("Hello") await asyncio.sleep(1) print("World")

asyncio.run(main())

Explain:

async

await

event loop

coroutine

Pattern Matching

match value:
    case 1:
        print("One")
    case 2:
        print("Two")

F-Strings

name = "John" print(f"Hello {name}")

Also:

f"{value:.2f}"

Standard Library Utilities

JSON processing

The json module is part of Python’s Standard Library, so it is built-in and does not need installation.
JSON is a serialized representation of a Python dictionary

import json

data = json.loads('{"a":1}')
json.dumps(data)

Conversion:

  • json.loads() β†’ JSON to dict
  • json.dumps() β†’ dict to JSON

pathlib (modern file paths)

#Better than os.path.

from pathlib import Path

p = Path("file.txt")
p.exists()

OOP Topics Missing

Inheritance

class Animal: pass

class Dog(Animal): pass

Polymorphism

class Cat: def speak(self): print("meow")

class Dog: def speak(self): print("bark")

Abstract Classes

from abc import ABC, abstractmethod

Python Internals (Advanced but Good)

Optional but useful.

Memory Management

reference counting

garbage collector

GIL (Global Interpreter Lock)

Important since you included threads.

Explain:

why Python threads don't run CPU tasks in parallel

multiprocessing alternative

Multiprocessing (Missing but Important)

Example:

from multiprocessing import Process

Testing (Very Useful)

Example:

import unittest

or

pytest

Closure

A closure is a function that remembers the variables from its outer (enclosing) scope even after the outer function has finished executing.

Closures are useful when you want to preserve some state without using a class.

def outer_function(msg):

    def inner_function():
        print(msg)   # inner function remembers 'msg'

    return inner_function


# Create closure
closure_func = outer_function("Hello Closure!")

# Call the returned function
closure_func()


#examples

Closures vs OOP

Closures can mimic some Object-Oriented Programming concepts by capturing variables from an enclosing scope.

Closures support: - Encapsulation - Data hiding - Stateful behavior - Function-based methods

Closures do not support: - Inheritance - Polymorphism - Class hierarchies

Closures are useful when lightweight stateful behavior is required without creating full classes.

Modules, Packages, Libraries

/MyProject
β”‚
└── /my_library (Library/Root)
    β”œβ”€β”€ __init__.py
    β”œβ”€β”€ /data_processing (Package)
    β”‚   β”œβ”€β”€ __init__.py
    β”‚   β”œβ”€β”€ cleaner.py (Module)
    β”‚   └── parser.py (Module)
    └── /visualization (Package)
        β”œβ”€β”€ __init__.py
        └── charts.py (Module)