Skip to content
EX-406 · Python/Quick Revision Short Notes

Python (EX-406) - Unit 3 Short Notes

UNIT 3: Python Programming - Core Concepts & Data Structures


3.1 Functions - The Building Blocks of Modular Code

Defining & Calling Functions

  • A function is defined using the def keyword, followed by a name (snake_case convention), parentheses (), and a colon :.

  • Parameters are variables listed in the function definition. Arguments are the actual values passed when calling.

    • Positional arguments: Must match the order of parameters.

    • Keyword arguments: Passed as name=value, order doesn't matter.

  • The return statement exits a function and optionally passes back an expression. A function can return multiple values as a tuple.

    
    def add(a, b):
    
        return a + b
    
    result = add(5, 3)  # Positional
    
    result = add(b=3, a=5)  # Keyword
    
    

Scope & Namespaces

  • Local scope: Variables defined inside a function. Accessible only within that function.

  • Global scope: Variables defined at the top level of a script or module.

  • global keyword: Used inside a function to modify a global variable.

  • nonlocal keyword: Used inside a nested function to modify a variable in the enclosing (non-global) scope.

    [!TIP] Common Pitfall: Using global excessively leads to code that is hard to debug and test. Prefer returning values and passing them as arguments.

Advanced Function Concepts

  • Default parameter values: Specified in the definition def func(a, b=10):. Evaluated once at function definition.

    [!CAUTION] Mutable Defaults: Using a mutable object (like [] or {}) as a default value is a classic error. The default is shared across all calls.

    
    def append_to(element, lst=[]):  # DANGEROUS
    
        lst.append(element)
    
        return lst
    
    

    Fix: Use None and create a new object inside.

    
    def append_to(element, lst=None):
    
        if lst is None:
    
            lst = []
    
        lst.append(element)
    
        return lst
    
    
  • Variable-length arguments:

    • *args: Collects extra positional arguments into a tuple.

    • **kwargs: Collects extra keyword arguments into a dictionary.

    
    def print_all(*args, **kwargs):
    
        print("Positional:", args)
    
        print("Keyword:", kwargs)
    
    print_all(1, 2, name="Alice", age=30)
    
    # Output: Positional: (1, 2)
    
    #         Keyword: {'name': 'Alice', 'age': 30}
    
    
  • Function annotations (type hints): Provide metadata about parameter and return types. Not enforced by Python but used by tools (mypy, IDEs).

    
    def greet(name: str) -> str:
    
        return f"Hello, {name}"
    
    
  • Lambda functions: Small, anonymous functions defined with lambda arguments: expression. Limited to a single expression. Commonly used with map(), filter(), sorted().

    
    square = lambda x: x**2
    
    nums = [1, 2, 3]
    
    squared = list(map(lambda x: x**2, nums))  # [1, 4, 9]
    
    sorted_by_second = sorted([(1, 2), (3, 1)], key=lambda x: x[1])
    
    

3.2 Essential Data Structures (Deep Dive)

Lists (Mutable Sequences)

Ordered, mutable collections. Created with [], list(), or comprehensions.

Method Description Example
append(x) Add item to end l.append(4)
extend(iter) Add all items from iterable l.extend([4,5])
insert(i, x) Insert at index i l.insert(0, 0)
remove(x) Remove first occurrence of x l.remove(3)
pop([i]) Remove & return item at i (default last) l.pop()
clear() Remove all items l.clear()
index(x) Return index of first x l.index(2)
count(x) Count occurrences of x l.count(1)
sort(key=None, reverse=False) Sort in-place l.sort(reverse=True)
reverse() Reverse in-place l.reverse()
  • List Comprehensions: Concise way to create lists.

    
    # Syntax: [expression for item in iterable if condition]
    
    squares = [x**2 for x in range(10)]
    
    even_squares = [x**2 for x in range(10) if x % 2 == 0]
    
    
  • Slicing: list[start:stop:step]. [::-1] reverses a list.

    
    lst = [0,1,2,3,4,5]
    
    print(lst[1:4])    # [1, 2, 3]
    
    print(lst[::2])    # [0, 2, 4]
    
    print(lst[::-1])   # [5, 4, 3, 2, 1, 0]
    
    
  • As Stack/Queue:

    • Stack (LIFO): Use append() (push) and pop() (pop).

    • Queue (FIFO): Use collections.deque for efficient popleft().

    
    from collections import deque
    
    q = deque()
    
    q.append(1)    # Enqueue
    
    q.append(2)
    
    q.popleft()    # Dequeue -> 1
    
    

Tuples (Immutable Sequences)

Ordered, immutable collections. Created with (), tuple(), or by comma separation.

  • Immutability: Cannot change, add, or remove items after creation.

  • Use Cases: Dictionary keys (since hashable), returning multiple values from functions, fixed collections.

  • Packing & Unpacking:

    
    # Packing
    
    t = 1, 2, 3  # Tuple (1, 2, 3)
    
    # Unpacking
    
    a, b, c = t  # a=1, b=2, c=3
    
    # Extended unpacking
    
    first, *middle, last = [1,2,3,4,5]  # first=1, middle=[2,3,4], last=5
    
    

Dictionaries (Mapping Types)

Unordered (as of Python 3.7+, insertion-ordered) collections of key:value pairs.

  • Key Requirements: Must be hashable (e.g., strings, numbers, tuples of immutables). Must be unique.

  • Core Methods Table:

Method Description
keys() Return view of keys
values() Return view of values
items() Return view of (key, value) pairs
get(key, default=None) Return value for key, or default if missing (no error)
pop(key, default) Remove & return value for key, or default
update(other) Merge other dict into this one
setdefault(key, default) If key exists, return its value. Else, insert key with default and return default.
clear() Remove all items
  • Dictionary Comprehension: {key_expr: value_expr for item in iterable}

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

    
    d = {'a':1, 'b':2}
    
    for key in d:           # Iterates keys
    
    for key in d.keys():
    
    for value in d.values():
    
    for key, value in d.items():
    
    

Sets (Unordered Collections of Unique Elements)

Unordered, mutable collections of unique, hashable elements. Created with {} (non-empty), set(), or from iterable.

  • Mathematical Operations:

    
    A = {1, 2, 3, 4}
    
    B = {3, 4, 5, 6}
    
    A | B   # Union: {1, 2, 3, 4, 5, 6}
    
    A & B   # Intersection: {3, 4}
    
    A - B   # Difference (in A not B): {1, 2}
    
    A ^ B   # Symmetric Difference: {1, 2, 5, 6}
    
    
  • Core Methods Table:

Method Description
add(x) Add element x
remove(x) Remove x; raises KeyError if missing
discard(x) Remove x if present; no error if missing
pop() Remove & return an arbitrary element
clear() Remove all elements
union(*others) / ` `
intersection(*others) / & Return intersection
  • Use Cases: Fast membership testing (x in my_set), removing duplicates from a list (list(set(duplicate_list))).

[!TIP] Exam Focus: Be prepared to write code using list comprehensions, dictionary get()/setdefault(), and set operations for data deduplication or comparison.


3.3 File Input/Output (I/O)

Opening & Closing Files

  • open(file, mode='r', encoding=None) returns a file object.

  • Common Modes:

    • 'r' : Read (default).

    • 'w' : Write (truncates existing file).

    • 'a' : Append.

    • 'x' : Exclusive creation.

    • 'b' : Binary mode.

    • 't' : Text mode (default).

    • Combine: 'rb', 'w+' (read/write), etc.

  • Always close files to free system resources. Two ways:

    1. Explicit: f.close()

    2. Recommended: Use with statement (context manager).

The with Statement (Context Managers)

  • Ensures the file is properly closed after the block, even if an exception occurs.

    
    with open('file.txt', 'r') as f:
    
        data = f.read()
    
    # File is automatically closed here
    
    

Reading from Files

  • f.read(size=-1): Read entire file or size bytes/characters.

  • f.readline(): Read a single line (including trailing newline).

  • f.readlines(): Read all lines into a list.

  • Iterating directly: Most memory-efficient for large files.

    
    with open('file.txt') as f:
    
        for line in f:
    
            print(line.strip())  # .strip() removes newline
    
    

Writing to Files

  • f.write(string): Write a string. Returns number of characters written.

  • f.writelines(seq_of_strings): Write a sequence of strings. Does not add newlines automatically.

    
    with open('out.txt', 'w') as f:
    
        f.write("Hello\n")
    
        f.writelines(["Line 1\n", "Line 2\n"])
    
    

Working with CSV & JSON

  • csv Module: For reading/writing comma-separated values.

    
    import csv
    
    # Reading
    
    with open('data.csv') as f:
    
        reader = csv.reader(f)
    
        for row in reader:  # Each row is a list of strings
    
            print(row)
    
    # Writing
    
    with open('out.csv', 'w', newline='') as f:
    
        writer = csv.writer(f)
    
        writer.writerow(['Name', 'Age'])
    
        writer.writerows([['Alice', 30], ['Bob', 25]])
    
    

    [!TIP] Always use newline='' when opening CSV files for writing to prevent extra blank lines on Windows.

  • json Module: For JavaScript Object Notation (serialization/deserialization).

    
    import json
    
    # Python -> JSON (Serialization)
    
    data = {'name': 'Alice', 'age': 30, 'pets': ['dog', 'cat']}
    
    json_str = json.dumps(data, indent=4)  # String with formatting
    
    # JSON -> Python (Deserialization)
    
    with open('data.json') as f:
    
        py_data = json.load(f)  # Returns dict/list
    
    

3.4 Error & Exception Handling

Syntax vs. Exceptions

  • Syntax Errors: Detected by parser before execution (e.g., missing colon, invalid syntax). Program won't run.

  • Exceptions: Errors detected during execution (runtime). Can be caught and handled.

try, except, else, finally Blocks

  • Structure:

    
    try:
    
        # Code that might raise an exception
    
        result = 10 / 0
    
    except ZeroDivisionError as e:
    
        # Handle specific exception
    
        print(f"Cannot divide by zero: {e}")
    
    except (TypeError, ValueError) as e:
    
        # Handle multiple specific exceptions
    
        print(f"Type or value error: {e}")
    
    except Exception as e:
    
        # Catch-all for any other exception (use cautiously)
    
        print(f"Unexpected error: {e}")
    
    else:
    
        # Executes if NO exception was raised in try block
    
        print("Division successful!")
    
    finally:
    
        # Executes ALWAYS, regardless of exception. For cleanup.
    
        print("Cleanup complete.")
    
    

    [!TIP] Order Matters: Catch specific exceptions before general ones (except Exception). A bare except: catches everything, including KeyboardInterrupt (Ctrl+C), making debugging hard.

Raising Exceptions

  • Use raise to trigger an exception manually.

    
    def validate_age(age):
    
        if age < 0:
    
            raise ValueError("Age cannot be negative")
    
    
  • Custom Exceptions: Create by subclassing Exception (or a built-in).

    
    class InsufficientFundsError(Exception):
    
        def __init__(self, balance, amount):
    
            self.balance = balance
    
            self.amount = amount
    
            super().__init__(f"Balance {balance} is insufficient for withdrawal {amount}")
    
    

Common Built-in Exceptions

Exception Typical Cause
ValueError Right type, inappropriate value (e.g., int('abc'))
TypeError Operation/function applied to wrong type (e.g., 'a' + 1)
IndexError Sequence subscript out of range
KeyError Dictionary key not found
FileNotFoundError open() on non-existent file in read mode
IOError General I/O failure (e.g., disk full)
AttributeError Attribute reference or assignment fails
ZeroDivisionError Division or modulo by zero
ImportError / ModuleNotFoundError import fails

3.5 Modules & Packages (Code Organization)

What is a Module?

  • A .py file containing Python definitions and statements.

  • Importing:

    
    import module_name           # Access via module_name.func()
    
    from module_name import func # Access directly via func()
    
    from module_name import *    # Imports all public names (AVOID: pollutes namespace)
    
    

The __name__ Variable

  • Special built-in variable.

  • When a module is run directly (e.g., python mymodule.py), __name__ is set to "__main__".

  • When a module is imported, __name__ is set to the module's name.

  • Idiomatic Pattern: Allows code to be both importable and executable.

    
    # mymodule.py
    
    def main():
    
        print("Running as a script")
    
    if __name__ == "__main__":
    
        main()
    
    

Standard Library Tour (Key Modules)

Module Common Uses
math sqrt(), pi, ceil(), floor(), log()
random randint(a,b), choice(seq), shuffle(seq)
datetime datetime.now(), date, timedelta
os os.getcwd(), os.listdir(), os.path.join()
sys sys.argv, sys.exit(), sys.path

Introduction to Packages

  • A package is a directory containing a special __init__.py file (can be empty) and one or more module files.

  • Hierarchical Imports:

    
    # Directory: mypackage/
    
    #   __init__.py
    
    #   module1.py
    
    #   subpackage/
    
    #       __init__.py
    
    #       module2.py
    
    from mypackage import module1
    
    from mypackage.subpackage import module2
    
    

    [!TIP] From Python 3.3+, namespace packages (without __init__.py) are supported for more flexible package structures.


3.6 Introduction to Object-Oriented Programming (OOP)

Why OOP?

  • Class: Blueprint/template for creating objects (defines attributes & methods).

  • Object/Instance: A concrete realization of a class.

  • Attribute: Variable associated with a class/object.

  • Method: Function defined inside a class that operates on objects.

Defining a Class


class Dog:

    # Class attribute (shared by all instances)

    species = "Canis lupus familiaris"

    def __init__(self, name, age):

        # Instance attributes (unique to each object)

        self.name = name

        self.age = age

    # Instance method

    def bark(self):

        return f"{self.name} says woof!"

    # Special method for string representation (for developers)

    def __repr__(self):

        return f"Dog(name='{self.name}', age={self.age})"

    # Special method for readable string representation (for end-users)

    def __str__(self):

        return f"{self.name} is {self.age} years old."

  • __init__(self, ...) is the constructor. self refers to the instance being created.

  • Instance Attributes: Defined with self.attr inside methods (usually __init__). Unique per object.

  • Class Attributes: Defined directly in the class body. Shared across all instances.

Defining Methods

  • Instance Methods: First parameter is self. Can access/modify instance attributes.

  • __str__() vs __repr__():

    • __str__(): "Official" string representation, readable. Called by print() and str().

    • __repr__(): "Unofficial" representation, often a valid Python expression to recreate the object. Called by repr() and in console.

Basic Inheritance

  • Subclassing: A child class inherits attributes and methods from a parent (base) class.

    
    class Animal:
    
        def __init__(self, name):
    
            self.name = name
    
        def speak(self):
    
            raise NotImplementedError("Subclasses must implement")
    
    class Cat(Animal):  # Cat inherits from Animal
    
        def speak(self):
    
            return f"{self.name} says Meow!"
    
    class Dog(Animal):
    
        def speak(self):
    
            return f"{self.name} says Woof!"
    
    
  • super(): Used in a child class to call methods from the parent class.

    
    class SpecialDog(Dog):
    
        def __init__(self, name, breed):
    
            super().__init__(name)  # Call parent's __init__
    
            self.breed = breed
    
    
  • Method Resolution Order (MRO): The order in which Python searches for methods in a hierarchy (especially with multiple inheritance). Access via ClassName.__mro__ or ClassName.mro().

[!TIP] Exam Focus: Be ready to define a simple class with __init__, instance attributes, and a method. Understand the difference between class and instance attributes. Know how to use super() in a single-inheritance scenario.

Go to where you left off?

Quick Add to Notes

Save questions, your own notes and screenshots into notes filed by unit. It takes a free account.

Create free account

Have an account? Log in