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
defkeyword, 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
returnstatement 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.
-
globalkeyword: Used inside a function to modify a global variable. -
nonlocalkeyword: Used inside a nested function to modify a variable in the enclosing (non-global) scope.[!TIP] Common Pitfall: Using
globalexcessively 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 lstFix: Use
Noneand 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 withmap(),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) andpop()(pop). -
Queue (FIFO): Use
collections.dequefor efficientpopleft().
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:
-
Explicit:
f.close() -
Recommended: Use
withstatement (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 orsizebytes/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
-
csvModule: 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. -
jsonModule: 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 bareexcept:catches everything, includingKeyboardInterrupt(Ctrl+C), making debugging hard.
Raising Exceptions
-
Use
raiseto 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
.pyfile 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__.pyfile (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.selfrefers to the instance being created. -
Instance Attributes: Defined with
self.attrinside 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 byprint()andstr(). -
__repr__(): "Unofficial" representation, often a valid Python expression to recreate the object. Called byrepr()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__orClassName.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 usesuper()in a single-inheritance scenario.