Python Unit 2 — Complete Master Guidebook | Python Unit 2
PYTHON PROGRAMMING
UNIT 2 — COMPLETE MASTER GUIDEBOOK
From Basics to PhD-Level Depth
Topics Covered
Functions: def, Parameters, Return Values, None, Keyword Arguments, Scope, global, Exception
Handling
Lists: List Data Type, Working with Lists, Augmented Assignment, Methods
Dictionaries: Dictionary Data Type, Pretty Printing, Data Structures for Real-World Modeling
Strings: Working with Strings, Useful String Methods
Python Unit 2 — Complete Master Guidebook | Python Unit 2
PART 1: FUNCTIONS
1. Functions in Python
A function is a named, reusable block of code that performs a specific task. Functions are the
fundamental unit of code organisation in any programming language. In Python, every function is a
first-class object — meaning functions can be passed as arguments, returned from other functions,
stored in variables, and stored inside data structures. This is a critical design decision that
separates Python from many statically-typed languages.
Why Do We Need Functions?
• Avoid code repetition (DRY — Don't Repeat Yourself principle)
• Break complex problems into smaller, manageable pieces (decomposition)
• Enable reusability across different parts of a program or projects
• Make code readable, testable, and maintainable
• Enable abstraction — hiding implementation details behind a clean interface
1.1 The def Statement
The def keyword (short for 'define') tells Python you are creating a function. The def statement
does NOT execute the function body; it only creates the function object and binds it to the given
name.
Syntax
def function_name(parameter1, parameter2, ...): # Function header
'''Docstring — describes what the function does'''
# Function body (indented block)
statement1
statement2
return value # Optional
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Deep-Dive: What Happens When Python Sees def?
When Python's interpreter encounters a def statement, it compiles the function body into a code
object, wraps it in a function object along with the current scope's variables, and assigns the whole
thing to the function name. This is why you can do:
def greet():
print('Hello!')
print(type(greet)) # <class 'function'>
print(greet) # <function greet at 0x...>
say_hi = greet # Functions are objects! Assign to another variable
say_hi() # Hello!
Simple Function Example
def greet_user(name):
'''Greets a user by their name.'''
print('Hello, ' + name + '!')
greet_user('Alice') # Output: Hello, Alice!
greet_user('Bob') # Output: Hello, Bob!
PhD-Level Question
Q: Python uses indentation to define blocks — unlike C/Java which use braces {}. What are the
advantages and disadvantages of indentation-based block delimiting?
Answer: ADVANTAGES — forces consistent style; eliminates 'brace wars'; code is visually clear.
DISADVANTAGES — mixing tabs and spaces causes IndentationError; makes copy-pasting
code fragile; can be confusing in deeply nested code. Python uses 4 spaces as the canonical
standard (PEP 8).
1.2 Parameters and Arguments
A parameter is the variable listed inside parentheses in the function definition. An argument is the
actual value passed to the function when it is called. This distinction is critical — parameters are
placeholders; arguments are the actual data.
Types of Parameters in Python
Parameter Type Syntax Description
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Positional def f(a, b) Arguments matched left-to-right by
position
Keyword f(a=1, b=2) Arguments matched by name, order
irrelevant
Default def f(a, b=10) Default value used if argument not
provided
*args def f(*args) Collects extra positional args into a
tuple
**kwargs def f(**kwargs) Collects extra keyword args into a dict
Positional-only (/) def f(a, b, /) Only positional allowed (Python 3.8+)
Keyword-only (*) def f(*, a, b) Only keyword arguments allowed
Detailed Code Examples for Each
# 1. Positional Parameters
def power(base, exponent):
return base ** exponent
print(power(2, 10)) # 1024 — base=2, exponent=10
# print(power(10, 2)) # 100 — ORDER matters with positional args
# 2. Default Parameters
def greet(name, greeting='Hello'):
print(f'{greeting}, {name}!')
greet('Alice') # Hello, Alice!
greet('Alice', 'Hi') # Hi, Alice!
# 3. *args — variable positional arguments
def total(*numbers):
print(type(numbers)) # <class 'tuple'>
return sum(numbers)
print(total(1, 2, 3)) # 6
print(total(10, 20, 30, 40))# 100
# 4. **kwargs — variable keyword arguments
def show_info(**details):
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print(type(details)) # <class 'dict'>
for key, value in [Link]():
print(f'{key}: {value}')
show_info(name='Alice', age=25, city='Delhi')
# name: Alice
# age: 25
# city: Delhi
CRITICAL: Mutable Default Arguments — A Common Trap!
def add_item(item, lst=[]): # WRONG! [] is created ONCE, shared across all calls
[Link](item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] <- Unexpected! The same list is reused.
# CORRECT approach: use None as default
def add_item(item, lst=None):
if lst is None:
lst = []
[Link](item)
return lst
WHY: Default parameter values are evaluated ONCE when the def statement runs,
not each time the function is called. Mutable defaults persist across calls.
1.3 Return Values and the return Statement
The return statement serves two purposes: (1) it immediately exits the function, and (2) it sends a
value back to the caller. A function without a return statement (or with a bare return) returns None
implicitly.
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Anatomy of a Return Statement
def add(a, b):
result = a + b
return result # Sends 'result' back to caller, function exits here
total = add(3, 4) # total receives the returned value 7
print(total) # 7
# Functions can return ANY Python object
def get_student():
return {'name': 'Alice', 'grade': 'A'} # Returns a dictionary
def get_scores():
return [95, 87, 92, 78] # Returns a list
def make_multiplier(factor):
def multiply(x): # Returns a FUNCTION (closure)
return x * factor
return multiply
double = make_multiplier(2)
print(double(5)) # 10
Multiple Return Values
Python functions can return multiple values. Under the hood, Python packs them into a tuple — this
is called tuple packing. The caller can unpack them.
def min_max(numbers):
return min(numbers), max(numbers) # Returns a tuple (min_val,
max_val)
result = min_max([3, 1, 9, 4, 7])
print(result) # (1, 9) -- it's actually a tuple!
print(type(result)) # <class 'tuple'>
# Tuple unpacking (most common usage)
low, high = min_max([3, 1, 9, 4, 7])
print(low, high) # 1 9
# Early exit with return
def find_index(lst, target):
Python Unit 2 — Complete Master Guidebook | Python Unit 2
for i, val in enumerate(lst):
if val == target:
return i # Exits immediately when found
return -1 # Only reached if not found
1.4 The None Value
In Python, None is the sole instance of the NoneType class. It represents the absence of a value or
a null value. None is not zero, not False, not an empty string — it is its own unique object.
Where None Appears
# 1. Functions without return statements return None
def do_nothing():
pass
result = do_nothing()
print(result) # None
print(type(result)) # <class 'NoneType'>
# 2. Variables with no meaningful value
user = None # Placeholder before value is assigned
# 3. Checking for None — ALWAYS use 'is', not '=='
if result is None:
print('Function returned nothing')
# WHY 'is' instead of '=='?
# 'is' checks identity (same object in memory)
# '==' checks equality (could be overridden by __eq__)
# None is a singleton — there is only ONE None in Python
# Using 'is None' is idiomatic, correct, and ~faster
# 4. None is falsy
if not None:
print('None is falsy!') # This prints
# 5. None in print()
print(None) # prints the string 'None'
x = print('Hello') # print() itself returns None!
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print(x) # None
PhD-Level Deep Dive: None vs False vs 0 vs ''
All four are 'falsy' in Python (bool(x) == False), but they are NOT equal to each other.
None == False → False | None is False → False
None == 0 → False | False == 0 → True (!) ← A surprising quirk
None == '' → False | False == '' → False
Why does False == 0? Because bool is a subclass of int in Python:
int(False) == 0 and int(True) == 1
This is a historical design decision for backwards compatibility.
Always use 'is None' for None checks, never '== None'.
1.5 Keyword Arguments and print()
Keyword arguments allow you to pass arguments to a function by name, regardless of their
position. This makes code more readable and less error-prone.
Keyword Arguments in Custom Functions
def describe_pet(animal_type, pet_name, age=0):
print(f'I have a {animal_type} named {pet_name}, age {age}.')
# Positional call
describe_pet('dog', 'Rex', 3)
# Keyword call — order doesn't matter!
describe_pet(pet_name='Rex', animal_type='dog', age=3)
# Mixed: positional first, then keyword
describe_pet('dog', pet_name='Rex', age=3)
# Rule: Positional args MUST come before keyword args
# describe_pet(pet_name='Rex', 'dog') # SyntaxError!
Python Unit 2 — Complete Master Guidebook | Python Unit 2
The print() Function — Full Anatomy
The built-in print() function signature is: print(*objects, sep=' ', end='\n',
file=[Link], flush=False). Each parameter is a keyword argument with a default value.
# Default behaviour
print('Hello', 'World') # Hello World
# sep — separator between arguments (default: single space)
print('Alice', 'Bob', 'Charlie', sep=', ') # Alice, Bob, Charlie
print('2024', '01', '15', sep='-') # 2024-01-15
print('A', 'B', 'C', sep='') # ABC
# end — what to print at the end (default: newline \n)
print('Loading', end='') # No newline
print('...') # ... (on same line as 'Loading')
# Building progress bars
import time
for i in range(5):
print(f'Step {i+1}', end=' -> ')
# Step 1 -> Step 2 -> Step 3 -> Step 4 -> Step 5 ->
# file — redirect output
import sys
print('Error message!', file=[Link]) # Writes to stderr, not stdout
# flush — force immediate output (useful for real-time display)
print('Downloading...', flush=True)
1.6 Local and Global Scope
Scope determines where a variable can be accessed. Python uses the LEGB Rule for variable
resolution: Local → Enclosing → Global → Built-in.
The LEGB Rule — Python's Variable Lookup Order
L — Local: Variables defined inside the current function
E — Enclosing: Variables in enclosing functions (for nested functions/closures)
G — Global: Variables defined at the module level (top of the file)
B — Built-in: Python's built-in names (print, len, range, etc.)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Python searches in this exact order. The FIRST match wins.
This is evaluated at RUNTIME, not at definition time.
Local Scope
def my_function():
x = 10 # x is LOCAL to my_function
print(x) # 10 — accessible inside
my_function()
# print(x) # NameError: name 'x' is not defined
# x doesn't exist outside the function
# Each function call creates a NEW local scope
def make_counter():
count = 0 # Fresh 'count' each call
count += 1
return count
print(make_counter()) # 1
print(make_counter()) # 1 (not 2! Each call gets a fresh scope)
Global Scope
team = 'Python Coders' # Global variable
def show_team():
print(team) # Can READ global variables from inside function
show_team() # Python Coders
# But CAN'T modify global without 'global' keyword
def try_modify():
team = 'New Team' # Creates a NEW LOCAL variable, doesn't touch
global
print(team) # New Team (local)
try_modify()
print(team) # Python Coders (global unchanged!)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Scope Conflict Example
x = 'global'
def outer():
x = 'enclosing' # Enclosing scope for inner()
def inner():
x = 'local' # Local scope
print(x) # 'local' -- LEGB: L wins
inner()
print(x) # 'enclosing'
outer()
print(x) # 'global'
1.7 The global Statement
The global statement tells Python that a variable name refers to the module-level global
variable, not a new local variable. Without it, assigning to a variable inside a function always
creates a local variable.
count = 0 # Global variable
def increment():
global count # Tell Python: 'count' here = the global 'count'
count += 1 # Modifies the global variable
increment()
increment()
increment()
print(count) # 3
# Multiple globals
name = ''
score = 0
def set_player(player_name, player_score):
global name, score
Python Unit 2 — Complete Master Guidebook | Python Unit 2
name = player_name
score = player_score
set_player('Alice', 95)
print(name, score) # Alice 95
Why Excessive Use of global is Considered Bad Practice
1. DEBUGGING NIGHTMARE: Any function can change global state — hard to track bugs
2. TIGHT COUPLING: Functions depend on external state, not just their parameters
3. NOT THREAD-SAFE: Multiple threads modifying same global → race conditions
4. TESTING DIFFICULTY: You must set global state before testing each function
Preferred alternatives:
• Pass values as parameters and return modified values
• Use class instances with instance variables (encapsulation)
• Use module-level constants (ALL_CAPS) which are never modified
Use global only when truly necessary — e.g., module-level configuration flags.
1.8 Exception Handling
An exception is an error that occurs during program execution. Without handling, exceptions crash
the program. Python's exception handling system uses try, except, else, and finally blocks to
gracefully manage errors.
The try-except Block
try:
# Code that might raise an exception
x = int(input('Enter a number: '))
result = 10 / x
print('Result:', result)
except ValueError:
print('That is not a valid number!')
except ZeroDivisionError:
print('Cannot divide by zero!')
Python Unit 2 — Complete Master Guidebook | Python Unit 2
except Exception as e:
print(f'An unexpected error occurred: {e}')
Full try-except-else-finally
try:
file = open('[Link]', 'r')
content = [Link]()
except FileNotFoundError:
print('File does not exist!')
content = ''
else:
# Runs ONLY if no exception was raised in try
print('File read successfully!')
print(f'Characters: {len(content)}')
finally:
# ALWAYS runs, exception or not
print('Done processing.')
# Typically used for cleanup: close files, release locks, etc.
Common Built-in Exceptions
Exception Cause Example
ValueError Wrong value type/range int('abc')
TypeError Wrong type for operation 'a' + 1
ZeroDivisionError Division by zero 5/0
IndexError List index out of range lst[100]
KeyError Dict key not found d['missing']
FileNotFoundError File does not exist open('[Link]')
NameError Undefined variable name print(undefined_var)
AttributeError Object lacks attribute 'str'.missing_method()
ImportError Module not found import non_existent
RecursionError Max recursion depth exceeded Infinite recursion
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Raising Custom Exceptions
# Raising built-in exceptions with custom messages
def set_age(age):
if not isinstance(age, int):
raise TypeError(f'Age must be an integer, got
{type(age).__name__}')
if age < 0 or age > 150:
raise ValueError(f'Age {age} is out of realistic range (0-150)')
return age
# Creating Custom Exception Classes
class InsufficientFundsError(Exception):
def __init__(self, amount, balance):
[Link] = amount
[Link] = balance
super().__init__(f'Cannot withdraw {amount}. Balance is only
{balance}.')
class BankAccount:
def __init__(self, balance):
[Link] = balance
def withdraw(self, amount):
if amount > [Link]:
raise InsufficientFundsError(amount, [Link])
[Link] -= amount
try:
account = BankAccount(100)
[Link](200)
except InsufficientFundsError as e:
print(e) # Cannot withdraw 200. Balance is only 100.
PhD-Level: Exception Hierarchy in Python
All exceptions inherit from BaseException. The main hierarchy is:
BaseException
├── SystemExit ([Link]() call)
├── KeyboardInterrupt (Ctrl+C)
├── GeneratorExit (generator/coroutine cleanup)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
└── Exception (ALL normal exceptions)
├── ArithmeticError → ZeroDivisionError, OverflowError
├── LookupError → IndexError, KeyError
├── ValueError, TypeError, OSError, ImportError, ...
NEVER catch BaseException or Exception broadly without re-raising!
Catching 'except Exception' silently swallows bugs.
Catch the MOST SPECIFIC exception you expect.
Python Unit 2 — Complete Master Guidebook | Python Unit 2
PART 2: LISTS
2. Lists in Python
A list is Python's most versatile, ordered, mutable sequence type. Lists can hold any mix of data
types, can be nested, and support a rich set of operations. Internally, Python lists are implemented
as dynamic arrays — they store references (pointers) to objects, not the objects themselves.
Key Properties of Python Lists
✓ Ordered — elements maintain their insertion order
✓ Mutable — elements can be added, removed, or changed after creation
✓ Heterogeneous — can hold elements of different types
✓ Dynamic — size grows and shrinks automatically
✓ Allow Duplicates — same value can appear multiple times
✓ Zero-indexed — first element is at index 0
2.1 The List Data Type
Creating Lists
# Empty list
empty = []
empty2 = list()
# List with values
fruits = ['apple', 'banana', 'cherry']
numbers = [1, 2, 3, 4, 5]
# Mixed types (Python allows this — other languages often don't)
mixed = [42, 'hello', 3.14, True, None, [1, 2]]
# List from other iterables
chars = list('hello') # ['h', 'e', 'l', 'l', 'o']
r = list(range(1, 6)) # [1, 2, 3, 4, 5]
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# List comprehension (compact, Pythonic)
squares = [x**2 for x in range(10)] # [0, 1, 4, 9, 16, 25, 36, 49, 64,
81]
# Nested list (2D grid)
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
Indexing and Slicing
fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
# 0 1 2 3 4
# -5 -4 -3 -2 -1
# Positive indexing (from start)
print(fruits[0]) # apple
print(fruits[2]) # cherry
# Negative indexing (from end)
print(fruits[-1]) # elderberry
print(fruits[-2]) # date
# Slicing: list[start:stop:step] (stop is EXCLUSIVE)
print(fruits[1:3]) # ['banana', 'cherry']
print(fruits[:3]) # ['apple', 'banana', 'cherry'] (start=0)
print(fruits[2:]) # ['cherry', 'date', 'elderberry'] (stop=end)
print(fruits[::2]) # ['apple', 'cherry', 'elderberry'] (every 2nd)
print(fruits[::-1]) # ['elderberry', 'date', 'cherry', 'banana', 'apple']
# (REVERSED! step=-1 walks backward)
# Slicing creates a SHALLOW COPY
copy = fruits[:] # Entire list copied
Mutability — Modifying List Elements
nums = [10, 20, 30, 40, 50]
# Change a single element
nums[0] = 100
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print(nums) # [100, 20, 30, 40, 50]
# Change a slice
nums[1:3] = [200, 300]
print(nums) # [100, 200, 300, 40, 50]
# Replace slice with different length
nums[1:3] = [999]
print(nums) # [100, 999, 40, 50] -- list shrank!
# Delete elements
del nums[0]
print(nums) # [999, 40, 50]
del nums[:] # Delete ALL elements (same as [Link]())
print(nums) # []
PhD-Level: Shallow Copy vs Deep Copy
a = [[1, 2], [3, 4]]
b = a[:] # Shallow copy — new outer list, SAME inner lists
b[0].append(99) # Modifies the INNER list, which is shared!
print(a) # [[1, 2, 99], [3, 4]] -- a was affected!
# For a true independent copy, use [Link]():
import copy
b = [Link](a) # Recursively copies all nested objects
b[0].append(99)
print(a) # [[1, 2], [3, 4]] -- a is unaffected
This distinction is critical in any real application handling nested data.
Python Unit 2 — Complete Master Guidebook | Python Unit 2
2.2 Working with Lists
Iterating Over Lists
fruits = ['apple', 'banana', 'cherry']
# Basic for loop
for fruit in fruits:
print(fruit)
# With index using enumerate()
for i, fruit in enumerate(fruits):
print(f'{i}: {fruit}')
# 0: apple 1: banana 2: cherry
# Iterate multiple lists simultaneously with zip()
names = ['Alice', 'Bob', 'Charlie']
scores = [90, 85, 92]
for name, score in zip(names, scores):
print(f'{name}: {score}')
# in operator — check membership
print('apple' in fruits) # True
print('mango' not in fruits) # True
List Operations
a = [1, 2, 3]
b = [4, 5, 6]
# Concatenation (creates new list)
c = a + b # [1, 2, 3, 4, 5, 6]
# Repetition
d = a * 3 # [1, 2, 3, 1, 2, 3, 1, 2, 3]
# Length
print(len(a)) # 3
# min, max, sum
nums = [5, 2, 8, 1, 9]
print(min(nums)) # 1
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print(max(nums)) # 9
print(sum(nums)) # 25
# Comparison
print([1,2,3] == [1,2,3]) # True (element-wise comparison)
print([1,2,3] < [1,2,4]) # True (lexicographic comparison)
2.3 Augmented Assignment Operators
Augmented assignment operators combine an operation with assignment. For lists, these are not
simply shorthand — they have important behavioural differences.
Operator Equivalent To Example
+= x = x + val (for lists: extend) lst += [4,5]
-= x = x - val (numbers only) n -= 1
*= x = x * val lst *= 2
/= x = x / val n /= 2
//= x = x // val n //= 3
%= x = x % val n %= 10
**= x = x ** val n **= 2
Critical Difference: += vs + for Lists
# For lists, += uses extend() INTERNALLY (in-place, same object)
a = [1, 2, 3]
original_id = id(a)
a += [4, 5] # Same object, extended
print(id(a) == original_id) # True — same list object!
print(a) # [1, 2, 3, 4, 5]
# For lists, + creates a NEW list
b = [1, 2, 3]
original_id = id(b)
b = b + [4, 5] # New list created, b now points to it
print(id(b) == original_id) # False — different object!
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# This matters for references!
lst1 = [1, 2]
lst2 = lst1 # Both point to same object
lst1 += [3] # In-place: lst2 is also affected!
print(lst2) # [1, 2, 3]
lst1 = [1, 2]
lst2 = lst1
lst1 = lst1 + [3] # New object: lst2 is NOT affected
print(lst2) # [1, 2]
2.4 List Methods
Python lists have a rich set of built-in methods. All mutating methods (append, remove, sort, etc.)
modify the list in place and return None. This is a common source of bugs for beginners.
Method What It Does Example
append(x) Add x to end [Link](99)
insert(i, x) Insert x at index i [Link](0, 'first')
extend(iterable) Add all items from iterable to end [Link]([4,5,6])
remove(x) Remove first occurrence of x (ValueError if [Link]('banana')
absent)
pop(i=-1) Remove & return item at index i (default: last) [Link]()
del lst[i] Remove item at index i (not a method, a del lst[2]
statement)
index(x) Return index of first occurrence of x [Link]('cherry')
count(x) Count occurrences of x [Link](5)
sort() Sort list in place [Link]()
sorted(lst) Return a NEW sorted list (does not mutate) sorted(lst)
reverse() Reverse list in place [Link]()
copy() Return shallow copy [Link]()
clear() Remove all elements [Link]()
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Deep Dive: sort() vs sorted()
nums = [3, 1, 4, 1, 5, 9, 2, 6]
# sort() — IN-PLACE, returns None
result = [Link]()
print(result) # None <- Common Bug! Don't assign sort()
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
# sorted() — creates NEW list, original unchanged
original = [3, 1, 4, 1, 5]
new = sorted(original)
print(original) # [3, 1, 4, 1, 5] -- unchanged!
print(new) # [1, 1, 3, 4, 5]
# Sorting with key function
words = ['banana', 'apple', 'cherry', 'date']
[Link](key=len) # Sort by string length
print(words) # ['date', 'apple', 'banana', 'cherry']
# Sort in reverse
[Link](key=len, reverse=True)
print(words) # ['banana', 'cherry', 'apple', 'date']
# Sort by custom key (e.g., sort list of dicts)
students = [
{'name': 'Alice', 'grade': 92},
{'name': 'Bob', 'grade': 87},
{'name': 'Charlie', 'grade': 95},
]
[Link](key=lambda s: s['grade'], reverse=True)
for s in students:
print(s['name'], s['grade'])
# Charlie 95 / Alice 92 / Bob 87
List Comprehensions — Pythonic Power
# Syntax: [expression for variable in iterable if condition]
# Basic: squares of 0-9
squares = [x**2 for x in range(10)]
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# With condition: even squares only
even_sq = [x**2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# Nested: flatten a 2D matrix
matrix = [[1,2,3],[4,5,6],[7,8,9]]
flat = [num for row in matrix for num in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# String processing
sentence = 'the quick brown fox'
words = [[Link]() for word in [Link]()]
# ['THE', 'QUICK', 'BROWN', 'FOX']
# WHY use comprehensions?
# - More readable than equivalent for loop
# - ~35% faster than equivalent loop (CPython implementation)
# - Returns a list, not a generator (unlike generator expressions)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
PART 3: DICTIONARIES & DATA STRUCTURES
3. Dictionaries and Structuring Data
A dictionary (dict) is Python's primary mapping type. It stores data as key-value pairs.
Dictionaries are implemented as hash tables — one of the most important data structures in
computer science. As of Python 3.7+, dictionaries maintain insertion order (this was made official
in Python 3.7, though CPython 3.6 already did it as an implementation detail).
Key Properties of Python Dictionaries
✓ Key-Value pairs — each key maps to exactly one value
✓ Keys must be HASHABLE (immutable): strings, numbers, tuples — NOT lists or dicts
✓ Values can be ANY Python object
✓ O(1) average-case lookup — constant time regardless of dict size (hash table)
✓ Ordered — insertion order preserved (Python 3.7+)
✓ No duplicate keys — assigning to existing key overwrites the value
✓ Dynamic — grow/shrink as needed
3.1 The Dictionary Data Type
Creating Dictionaries
# Empty dict
empty = {}
empty2 = dict()
# Dict literal
person = {'name': 'Alice', 'age': 30, 'city': 'Delhi'}
# Using dict() constructor with keyword args
person2 = dict(name='Bob', age=25, city='Mumbai')
# From list of (key, value) tuples
pairs = [('a', 1), ('b', 2), ('c', 3)]
Python Unit 2 — Complete Master Guidebook | Python Unit 2
d = dict(pairs) # {'a': 1, 'b': 2, 'c': 3}
# Dict comprehension
squares = {x: x**2 for x in range(6)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Nested dict
students = {
'alice': {'grade': 92, 'major': 'CS'},
'bob': {'grade': 87, 'major': 'Math'},
}
Accessing Values
person = {'name': 'Alice', 'age': 30, 'city': 'Delhi'}
# Direct access — raises KeyError if key absent
print(person['name']) # Alice
# print(person['salary']) # KeyError!
# get() — returns None (or default) if key absent
print([Link]('age')) # 30
print([Link]('salary')) # None (no exception!)
print([Link]('salary', 0))# 0 (custom default)
# WHY prefer get()? Safety! Use [] only when key is guaranteed to exist.
Modifying Dictionaries
person = {'name': 'Alice', 'age': 30}
# Add new key-value pair
person['email'] = 'alice@[Link]'
# Update existing key
person['age'] = 31
# Update multiple keys at once
[Link]({'age': 32, 'city': 'Delhi', 'job': 'Engineer'})
# Remove a key
Python Unit 2 — Complete Master Guidebook | Python Unit 2
del person['email'] # KeyError if key doesn't exist
job = [Link]('job') # Remove and RETURN value
item = [Link]() # Remove and return LAST inserted (key,
value) pair
# setdefault — set only if key doesn't exist
[Link]('salary', 50000) # Adds 'salary':50000 ONLY if not
present
[Link]('age', 99) # Does nothing — 'age' already exists
Iterating Over Dictionaries
person = {'name': 'Alice', 'age': 30, 'city': 'Delhi'}
# Iterate over keys (default iteration)
for key in person:
print(key) # name, age, city
# Explicit keys()
for key in [Link]():
print(key)
# Iterate over values
for value in [Link]():
print(value) # Alice, 30, Delhi
# Iterate over key-value pairs — MOST COMMON pattern
for key, value in [Link]():
print(f'{key}: {value}')
# Check if key exists
print('name' in person) # True
print('salary' in person) # False
# 'in' checks KEYS only (not values)
print('Alice' in person) # False — 'Alice' is a value, not a key
Method Returns Description
keys() dict_keys view All keys (dynamic view object)
values() dict_values view All values (dynamic view)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
items() dict_items view All (key, value) tuples (dynamic view)
get(k, default) value or default Safe access, no KeyError
pop(k) value Remove key, return value
popitem() (key, value) Remove and return last item
update(dict2) None Merge dict2 into this dict
setdefault(k, v) value Set key if absent, return value
clear() None Remove all items
copy() dict Shallow copy
fromkeys(keys, val) dict Create dict with given keys, same val
3.2 Pretty Printing
The pprint module (pretty print) formats complex nested data structures in a human-readable way.
It automatically adds line breaks and indentation for deeply nested structures.
import pprint
data = {
'users': [
{'id': 1, 'name': 'Alice', 'scores': [92, 87, 95]},
{'id': 2, 'name': 'Bob', 'scores': [78, 85, 80]},
],
'total': 2,
'course': 'Python Programming'
}
# Normal print — hard to read
print(data)
# pprint — nicely formatted
[Link](data)
# pprint with options
[Link](data, indent=4, width=60, depth=2, sort_dicts=False)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# pformat — get pretty string (for logging/saving)
formatted = [Link](data, indent=2)
print(type(formatted)) # <class 'str'>
# Using [Link] for even cleaner output
import json
print([Link](data, indent=4))
3.3 Using Data Structures to Model Real-World Things
Example 1: Representing a Deck of Cards
import random
# Model a card deck as a list of dictionaries
suits = ['Spades', 'Hearts', 'Diamonds', 'Clubs']
ranks = ['2','3','4','5','6','7','8','9','10','J','Q','K','A']
deck = [{'suit': suit, 'rank': rank} for suit in suits for rank in ranks]
print(len(deck)) # 52
print(deck[0]) # {'suit': 'Spades', 'rank': '2'}
# Shuffle
[Link](deck)
# Deal 5 cards
hand = [[Link]() for _ in range(5)]
for card in hand:
print(f"{card['rank']} of {card['suit']}")
Example 2: Student Grade Tracker
# Nested data structure: dict of dicts
gradebook = {}
def add_student(name):
gradebook[name] = {'scores': [], 'average': 0}
def add_score(name, score):
Python Unit 2 — Complete Master Guidebook | Python Unit 2
if name not in gradebook:
raise KeyError(f'Student {name} not found')
gradebook[name]['scores'].append(score)
scores = gradebook[name]['scores']
gradebook[name]['average'] = sum(scores) / len(scores)
def top_students(n=3):
return sorted([Link](),
key=lambda item: item[1]['average'],
reverse=True)[:n]
add_student('Alice')
add_student('Bob')
add_score('Alice', 95)
add_score('Alice', 88)
add_score('Bob', 79)
add_score('Bob', 84)
import pprint
[Link](gradebook)
print('Top students:', top_students())
PhD-Level: How Python Dictionaries Work Internally (Hash Tables)
When you do d[key], Python computes hash(key), then:
1. Calculates slot = hash(key) % table_size
2. Looks up that slot in the internal array
3. If the slot is occupied by a different key (hash collision), it probes
adjacent slots (open addressing with pseudo-random probing)
WHY keys must be hashable (immutable):
- If a key changed after insertion, hash(key) would change
- Python would look in the wrong slot and never find the value
- Lists change → not hashable. Tuples don't → hashable.
Average case O(1), worst case O(n) (all keys hash to same slot).
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Python's dict has a load factor of ~2/3 — it resizes when 67% full.
Resizing doubles capacity and rehashes all entries.
Python Unit 2 — Complete Master Guidebook | Python Unit 2
PART 4: MANIPULATING STRINGS
4. Manipulating Strings
Strings in Python are immutable sequences of Unicode characters. Every string operation
returns a new string — the original is never modified. Strings support indexing, slicing, iteration,
and a large library of methods.
Key Properties of Python Strings
✓ Immutable — cannot modify characters in place; every operation creates a new string
✓ Unicode — Python 3 strings are full Unicode (UTF-8 by default), handling ALL world
languages
✓ Sequence — supports indexing, slicing, len(), iteration, in operator
✓ Interning — Python caches (interns) small/identifier-like strings for efficiency
✓ Ordered — characters maintain their position
4.1 Working with Strings
Creating Strings
# Single and double quotes (interchangeable)
s1 = 'Hello World'
s2 = "Hello World"
# Triple quotes — multi-line strings and docstrings
multi = '''This is
a multi-line
string.'''
# Raw strings — backslashes treated literally
path = r'C:\Users\Alice\Documents'
regex = r'\d+\.\d+'
# Byte strings
b = b'Hello' # <class 'bytes'> — NOT a str
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# f-strings (Python 3.6+) — format string
name = 'Alice'
age = 30
msg = f'Hello {name}, you are {age} years old!'
# f-string expressions
print(f'2 + 2 = {2 + 2}') # 2 + 2 = 4
print(f'{[Link]()!r}') # 'ALICE'
print(f'{3.14159:.2f}') # 3.14
print(f'{1000000:,}') # 1,000,000
print(f'{'left':<10}|{'right':>10}') # left | right
String Indexing and Slicing
text = 'Python'
# 012345
# -6-5-4-3-2-1
print(text[0]) # P
print(text[-1]) # n
print(text[1:4]) # yth
print(text[::-1]) # nohtyP (reversed!)
print(text[::2]) # Pto
# Strings are immutable — cannot assign to index
# text[0] = 'J' # TypeError!
# To modify, slice and concatenate
new_text = 'J' + text[1:] # 'Jython'
String Operators
# Concatenation
s = 'Hello' + ' ' + 'World' # Hello World
# Repetition
s = 'ha' * 3 # hahaha
# Membership
print('ell' in 'Hello') # True
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print('xyz' not in 'Hello') # True
# Comparison (lexicographic / Unicode code point order)
print('apple' < 'banana') # True (a < b)
print('abc' == 'abc') # True
print('Z' < 'a') # True (ord('Z')=90, ord('a')=97)
# len()
print(len('Hello')) # 5
String Escape Characters
Escape Sequence Meaning Example Output
\n Newline Hello\nWorld → Hello (newline) World
\t Horizontal tab A\tB → A B
\\ Literal backslash C:\\path → C:\path
\' Literal single quote 'It\'s fine'
\" Literal double quote "He said \"hi\"
\r Carriage return Used in Windows line endings
\0 Null character String terminator in C; valid in Python
\uXXXX Unicode code point (4 hex) \u03B1 → α
\UXXXXXXXX Unicode code point (8 hex) \U0001F600 → 😀
4.2 Useful String Methods
Python strings have over 40 built-in methods. Remember: all string methods return a new string
— they never modify the original.
Case Methods
s = 'hello world python'
print([Link]()) # HELLO WORLD PYTHON
print([Link]()) # hello world python
print([Link]()) # Hello World Python (each word capitalised)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print([Link]()) # Hello world python (only first char of string)
print([Link]()) # HELLO WORLD PYTHON -> hello world python
# isupper(), islower(), istitle()
print('HELLO'.isupper()) # True
print('hello'.islower()) # True
print('Hello World'.istitle()) # True
Search and Check Methods
s = 'Hello, World!'
# find() — returns index of FIRST occurrence, or -1 if not found
print([Link]('o')) # 4
print([Link]('xyz')) # -1 (safe — no exception)
# index() — like find() but raises ValueError if not found
print([Link]('o')) # 4
# [Link]('xyz') # ValueError!
# rfind() / rindex() — search from RIGHT
print([Link]('o')) # 8 (the 'o' in 'World')
# count() — count non-overlapping occurrences
print([Link]('l')) # 3
print('banana'.count('an'))# 2 (non-overlapping!)
# startswith() / endswith()
print([Link]('Hello')) # True
print([Link]('!')) # True
print([Link](('Hi', 'Hello'))) # True (tuple of options!)
# Char-type checking
print('123'.isdigit()) # True
print('abc'.isalpha()) # True
print('abc123'.isalnum()) # True
print(' '.isspace()) # True
Modification Methods (Return New Strings)
# strip(), lstrip(), rstrip() — remove whitespace (or specific chars)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
s = ' Hello '
print([Link]()) # 'Hello'
print([Link]()) # 'Hello '
print([Link]()) # ' Hello'
# Strip specific characters
print('###Hello###'.strip('#')) # Hello
print('/path/to/file/'.strip('/'))# path/to/file
# replace(old, new, count=-1)
s = 'foo bar foo baz foo'
print([Link]('foo', 'qux')) # qux bar qux baz qux
print([Link]('foo', 'qux', 2)) # qux bar qux baz foo (only first 2)
# split() — split string into list
s = 'apple,banana,cherry'
print([Link](',')) # ['apple', 'banana', 'cherry']
print([Link](',', 1)) # ['apple', 'banana,cherry'] (max 1 split)
# split() on whitespace (default) handles multiple spaces
print(' a b c '.split()) # ['a', 'b', 'c']
# splitlines() — split at line boundaries
text = 'line1\nline2\nline3'
print([Link]()) # ['line1', 'line2', 'line3']
# join() — join list into string
words = ['Python', 'is', 'awesome']
print(' '.join(words)) # Python is awesome
print('-'.join(words)) # Python-is-awesome
print(''.join(words)) # Pythonisawesome
# WHY join() on separator, not on list?
# Because different separators can be used efficiently.
# It also works on any iterable, not just lists.
Justification and Padding Methods
# ljust(width, fillchar), rjust(width, fillchar), center(width, fillchar)
s = 'Python'
print([Link](15)) # 'Python ' (padded right with spaces)
Python Unit 2 — Complete Master Guidebook | Python Unit 2
print([Link](15)) # ' Python' (padded left with spaces)
print([Link](15)) # ' Python ' (centred)
print([Link](15, '*')) # '****Python*****' (centred with *)
# zfill(width) — pad with zeros on the left
print('42'.zfill(8)) # 00000042
print('-42'.zfill(8)) # -0000042 (sign preserved!)
String Formatting — All Methods Compared
name = 'Alice'
score = 95.678
# Method 1: % formatting (old, C-style — avoid in new code)
print('Name: %s, Score: %.2f' % (name, score))
# Method 2: [Link]() (Python 2.6+)
print('Name: {}, Score: {:.2f}'.format(name, score))
print('Name: {n}, Score: {s:.2f}'.format(n=name, s=score)) # Named
# Method 3: f-strings (Python 3.6+) — PREFERRED
print(f'Name: {name}, Score: {score:.2f}')
# Format specifiers inside f-strings
pi = 3.14159265
print(f'{pi:.2f}') # 3.14 (2 decimal places)
print(f'{pi:10.3f}') # ' 3.142' (width 10, 3 decimal places)
print(f'{1000000:,}') # 1,000,000 (thousands separator)
print(f'{0.456:.1%}') # 45.6% (percentage)
print(f'{255:#x}') # 0xff (hex with prefix)
print(f'{42:08b}') # 00101010 (binary, zero-padded to 8)
PhD-Level: String Immutability and Performance
Because strings are immutable, concatenating N strings in a loop is O(N²):
result = ''
for word in words:
result += word # Creates a NEW string each iteration!
Python Unit 2 — Complete Master Guidebook | Python Unit 2
For N words, total memory copied = 1+2+3+...+N = N(N+1)/2 → O(N²) time.
The Pythonic, O(N) solution:
result = ''.join(words) # join() pre-allocates exactly the right buffer
String interning: Python automatically interns string literals that look
like identifiers (all letters/digits/underscore). Two such strings may
share the same memory object (is returns True). Use [Link]() to
manually intern strings for performance in lookup-heavy applications.
String Methods Summary Table
Category Method Purpose
Case upper() / lower() Convert to all upper/lower case
Case title() / capitalize() Title-case or sentence-case
Case swapcase() Flip every character's case
Search find() / rfind() Return index of substring (-1 if not found)
Search index() / rindex() Return index (ValueError if not found)
Search count(sub) Count non-overlapping occurrences
Check startswith() / endswith() Boolean start/end check
Check isdigit() / isalpha() Check character content
Check isspace() / isalnum() All spaces / all alphanumeric
Modify strip() / lstrip() / rstrip() Remove whitespace/characters from ends
Modify replace(old, new) Replace all occurrences of old
Split/Join split(sep) Split into list of strings
Split/Join splitlines() Split at newline boundaries
Split/Join join(iterable) Join iterable into one string
Pad ljust() / rjust() / center() Pad to fixed width
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Pad zfill(width) Pad with leading zeros
Encode encode(encoding) Convert str to bytes
4.3 Advanced String Topics for PhD-Level Depth
Regular Expressions (re module)
import re
text = 'My phone is 98765-43210 and my email is alice@[Link]'
# Search for pattern
match = [Link](r'\d{5}-\d{5}', text)
if match:
print('Phone found:', [Link]()) # 98765-43210
# Find all matches
emails = [Link](r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
text)
print(emails) # ['alice@[Link]']
# Replace with regex
clean = [Link](r'\d', 'X', text) # Replace all digits with X
# Split on pattern
parts = [Link](r'\s+', 'a b c d')
print(parts) # ['a', 'b', 'c', 'd']
String vs Bytes
# In Python 3, str and bytes are COMPLETELY separate types
s = 'Hello' # str — sequence of Unicode code points
b = b'Hello' # bytes — sequence of integers 0-255
# Convert str → bytes (encode)
b = [Link]('utf-8') # b'Hello'
b_utf16 = [Link]('utf-16') # Different bytes!
# Convert bytes → str (decode)
s2 = [Link]('utf-8') # 'Hello'
Python Unit 2 — Complete Master Guidebook | Python Unit 2
# WHY does this matter?
# File I/O: text files = str, binary files = bytes
# Network: sockets send/receive bytes
# A '\u4e2d' (Chinese char) encodes to 3 bytes in UTF-8
chinese = '\u4e2d\u6587'
print(len(chinese)) # 2 characters
print(len([Link]('utf-8'))) # 6 bytes!
EXAM PREPARATION: Quick Reference &
Common Traps
5. Common Exam Questions & PhD-Level Traps
Functions — Must-Know Points
• def creates a function object — it is NOT called immediately
• return None is implicit in any function without a return statement
• Mutable default arguments are shared across all calls — use None instead
• *args collects extra positional args as a tuple; **kwargs collects keyword args as a dict
• global is needed to MODIFY a global variable, not just READ it
• try-except-else-finally: else runs only when NO exception; finally ALWAYS runs
• LEGB Rule: Python resolves names as Local → Enclosing → Global → Built-in
• Functions are first-class objects — can be assigned to variables, passed as args, returned
Lists — Must-Know Points
• sort() returns None and sorts in place; sorted() returns a NEW list
• += on lists uses extend() in-place (same object); + creates a new list
• Shallow copy (lst[:] or [Link]()) shares nested objects; use [Link]() for
independence
• Negative indexing: lst[-1] is last element; lst[::-1] reverses
• List comprehension is faster than equivalent for loop (Python optimization)
• append() adds ONE item; extend() adds ALL items from iterable; append([1,2]) adds a
nested list!
Python Unit 2 — Complete Master Guidebook | Python Unit 2
Dictionaries — Must-Know Points
• Keys must be hashable — lists/dicts cannot be keys; strings, numbers, tuples CAN be
• d[key] raises KeyError; [Link](key) returns None (safe)
• in operator on dict checks keys only, not values
• [Link]() returns view objects — they reflect current dict state dynamically
• Insertion order preserved since Python 3.7+
• Hash tables: average O(1) lookup; worst case O(n) on hash collision
Strings — Must-Know Points
• Immutability: every string method returns a NEW string; original unchanged
• find() returns -1 if not found; index() raises ValueError — choose based on need
• ''.join(list) is O(N) and preferred over += concatenation in loops (O(N²))
• f-strings are the modern, preferred formatting method (Python 3.6+)
• split() with no arg splits on any whitespace and ignores multiple spaces; split(' ') is
different!
• strip() removes characters from BOTH ends; characters not a prefix/suffix, just a set
Top PhD-Level 'Why' Questions to Prepare For
1. Why is None a singleton? → Only one None object exists in memory; 'is None' is identity
check
2. Why are strings immutable? → Thread-safety, hashing (can be dict keys), memory efficiency
(interning)
3. Why does Python use duck typing instead of strict typing? → Flexibility, polymorphism without
inheritance
4. Why does bool inherit from int? → Historical C compatibility; True==1 and False==0
5. Why is dict lookup O(1)? → Hash tables; hash(key) maps directly to memory slot
6. Why does += on lists modify in place but + creates new? → __iadd__ vs __add__ protocols
7. Why does global scope not mean accessible everywhere? → Module scope; each module has
its own globals()
8. Why does mutable default argument persist? → Evaluated once at def-time, not at call-time
9. Why is join() on separator string, not on list? → Works on ANY iterable; different separator
styles
10. Why does Python have both find() and index()? → Graceful handling vs fail-fast; different use
cases
Python Unit 2 — Complete Master Guidebook | Python Unit 2