0% found this document useful (0 votes)
4 views41 pages

Python Unit2 Guidebook

The document is a comprehensive guide on Python programming, specifically focusing on functions, lists, dictionaries, and strings. It covers essential concepts such as function definitions, parameters, return values, scope, and exception handling, providing detailed examples and explanations. The guide is structured to cater to learners from basic to advanced levels, including PhD-level discussions.

Uploaded by

voparo7158
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views41 pages

Python Unit2 Guidebook

The document is a comprehensive guide on Python programming, specifically focusing on functions, lists, dictionaries, and strings. It covers essential concepts such as function definitions, parameters, return values, scope, and exception handling, providing detailed examples and explanations. The guide is structured to cater to learners from basic to advanced levels, including PhD-level discussions.

Uploaded by

voparo7158
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

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

You might also like