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

CS50 Python Complete Book

CS50 Python is a comprehensive course companion designed for beginners to learn Python programming, inspired by Harvard's CS50 course. The book covers essential concepts in a structured manner, including variables, data types, conditionals, loops, functions, exceptions, file handling, libraries, regular expressions, and object-oriented programming. It emphasizes clarity and understanding, with no prerequisites required, making it accessible for anyone interested in coding.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views85 pages

CS50 Python Complete Book

CS50 Python is a comprehensive course companion designed for beginners to learn Python programming, inspired by Harvard's CS50 course. The book covers essential concepts in a structured manner, including variables, data types, conditionals, loops, functions, exceptions, file handling, libraries, regular expressions, and object-oriented programming. It emphasizes clarity and understanding, with no prerequisites required, making it accessible for anyone interested in coding.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

CS50 Python

A Complete Course Companion

From Variables to Object-Oriented Programming

Written in the spirit of David J. Malan & Harvard's CS50

A Lifetime Reference for Every Python Programmer

© 2025 • All concepts taught with clarity, depth, and care


CHAPTER

Preface

"The most important thing I can do as a teacher is make you believe you can do this. Because you can."

To the Reader
This book was written for one person: someone who opened a Python file for the first time and thought, "I'm not sure I'm
smart enough for this." You are. Every concept in this book — no matter how abstract it sounds — can be understood
deeply, completely, and permanently. That is this book's only promise.

Inspired by Harvard's legendary CS50 course and the teaching philosophy of Professor David J. Malan, this book does not
just show you what Python does. It shows you why every keyword exists, why every rule is the way it is, and what is actually
happening inside your computer when you press Enter. That kind of understanding does not fade. You can return to this
book ten years from now and pick up exactly where you left off.

How This Book Is Organized


The book is divided into nine chapters, each covering one major topic in Python. Every chapter follows the same structure:

• Intuition First — A real-life analogy to anchor the concept before any code appears.
• Deep Explanation — What is happening in memory, in the CPU, in Python's brain.
• Syntax Breakdown — Every keyword, every symbol, explained word by word.
• Worked Examples — From the simplest possible case to sophisticated real-world programs.
• Common Mistakes — The errors every beginner (and many experts) make, and exactly why they happen.
• Practice Problems — Three levels: Easy, Medium, and Challenging, each with full solutions.
• Chapter Summary — A crisp revision card you can use for quick review.

A Note on Style
This book speaks in the second person — you. It is written as if a professor is sitting across from you, explaining things on a
whiteboard, occasionally cracking a joke, and always making sure you understood the last thing before moving to the next.
Code is formatted in a monospace font and presented with explanations that walk through it line by line — sometimes
character by character. ASCII diagrams show how data moves. Callout boxes highlight important warnings and tips.

Prerequisites
Absolutely none. This book assumes you have never written a single line of code. If you have, you will move quickly through
the early chapters and find the later ones challenging and rewarding. Either way, there is something here for you.

Now, let's begin. Chapter 1 is waiting. And it's not as scary as you think.
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER

Table of Contents

Preface —

Chapter 1 — Variables and Data Types


What is a Variable?
Naming Variables
Core Data Types
Type Checking & Conversion
User Input
Common Mistakes

Chapter 2 — Conditionals
The if Statement
if / else
elif Chains
Comparison & Logical Operators
Nested Conditionals
Pythonic Conditions
The match Statement

Chapter 3 — Loops
The while Loop
The for Loop
range()
break, continue, pass
Nested Loops
Loop Patterns

Chapter 4 — Functions
Defining Functions
Parameters & Arguments
Return Values
Scope
Default Arguments
Variable Arguments
Lambda Functions

—3—
CS50 Python — A Complete Course Companion CS50 Python

Recursion

Chapter 5 — Exceptions
What Are Exceptions?
try / except
else & finally
Raising Exceptions
Custom Exception Classes

Chapter 6 — File Handling


Opening & Closing Files
Reading Files
Writing Files
CSV Files
JSON Files
Binary Files

Chapter 7 — Libraries & Modules


Importing Modules
The Standard Library
Third-Party Packages
Creating Your Own Modules
Key Libraries Overview

Chapter 8 — Regular Expressions


What Are Regular Expressions?
The re Module
Patterns & Quantifiers
Groups & Capturing
[Link] vs [Link] vs [Link]
Practical Applications

Chapter 9 — Object-Oriented Programming


Why OOP?
Classes & Objects
The __init__ Method
Instance vs Class Attributes
Methods
Inheritance
Encapsulation
Special Methods

—4—
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 1

Variables and Data Types

"Before a computer can think, it needs somewhere to remember. Variables are


that memory — and data types are the shape of what is stored."

1.1 What Is a Variable?

Analo Imagine a giant warehouse full of labeled boxes. Each box can hold one
gy thing — a number, a name, a list. You, the programmer, decide what goes
in each box and what label it gets. That warehouse is your computer's RAM.
Those labeled boxes are variables.

A variable is a named reference to a location in computer memory that stores a value. The
name stays the same; the value can change at any time. That combination — a stable name
pointing to a changeable value — is one of the most powerful ideas in all of programming.

Formal Definition
In Python, a variable is created the moment you assign a value to it using the equals sign (=).
There is no separate 'declaration' step as in some other languages. You simply write:

name = "Alice"
age = 21
gpa = 3.85

Creating three variables in Python

What Happens Inside the Computer


Python does not store the value directly inside the variable name. Instead, it works in three
steps:

1. Python creates a value object somewhere in memory (e.g., the string "Alice" at address
0x7f3a).
2. Python creates a name binding — an entry in a lookup table that maps the label "name"
to that address.

—5—
CS50 Python — A Complete Course Companion CS50 Python

3. Every time you use the variable name in your code, Python follows the pointer to retrieve
the value.

Your Code Python's Name Table RAM


--------- ---------------- ---
name = "Alice" -> name ---------> [0x7f3a] "Alice"
age = 21 -> age ---------> [0x7f4c] 21
gpa = 3.85 -> gpa ---------> [0x7f58] 3.85

This model is called reference semantics. Variables in Python are not boxes containing
values; they are labels pointing to values. This distinction matters enormously when you reach
functions and object-oriented programming.

1.2 Naming Variables


Python enforces strict rules about variable names. Break them and you get an error. Beyond
the rules, the Python community follows style conventions (PEP 8) that make code readable to
other humans.

The Rules (Mandatory)

Rule Valid Example Invalid Example

Must start with a letter or underscore name, _count 1score, 2fast

Can contain letters, digits, underscores


score_1, player2 my-score, [Link]

Cannot contain spaces first_name first name

Case sensitive Name ≠ name ≠ NAME —

Cannot be a Python keyword my_if, my_for if, for, while, True

The Conventions (PEP 8 Style Guide)


PEP 8 is Python's official style guide. Following it makes your code look professional and
readable to any Python programmer in the world.

• Use snake_case for variable names: words separated by underscores, all lowercase.
Example: student_name, total_price_usd.
• Make names descriptive. n is cryptic; number_of_students is self-documenting.
• Avoid single-letter names except for short loops or mathematical variables.
• Constants (values that never change) use ALL_CAPS: MAX_RETRIES = 3.

—6—
CS50 Python — A Complete Course Companion CS50 Python

# Good naming — self-documenting code


student_name = "Alice"
total_price_usd = 99.99
is_logged_in = True
MAX_CONNECTIONS = 10

# Bad naming — cryptic and confusing


s = "Alice"
tp = 99.99
b = True

1.3 The Core Data Types

Analo Not all boxes are created equal. A box for eggs has compartments. A box
gy for shoes is rectangular and firm. A box for liquids is sealed. In Python,
every piece of data has a TYPE — and that type determines what you can
do with it and how it behaves.

Python has five data types you will use constantly. Each is described in detail below.

1.3.1 str — String (Text Data)


A string is any sequence of characters — letters, digits, symbols, spaces — wrapped in single
or double quotes. The name 'string' comes from the idea of characters strung together like
beads on a necklace.

first_name = "Harry"
last_name = 'Potter' # single quotes also work
full_name = "Harry" + " " + "Potter" # concatenation with +
greeting = f"Hello, {first_name}!" # f-string (modern, preferred)

The f-string (formatted string literal) is Python's most elegant way to embed variables inside
text. The f prefix tells Python: "look inside this string for curly braces, and substitute any
variable names you find."

f"Hello, {first_name}!"
| | |
| | Literal text: '!'
| Python looks up 'first_name' and inserts its value

—7—
CS50 Python — A Complete Course Companion CS50 Python

The 'f' prefix activates f-string interpolation

Result: "Hello, Harry!"

Key string operations:

name = "alice"
print([Link]()) # "ALICE" — all uppercase
print([Link]()) # "Alice" — first letter capitalized
print([Link]()) # "alice" — all lowercase
print(len(name)) # 5 — number of characters
print(name[0]) # "a" — first character (index 0)
print(name[-1]) # "e" — last character (negative index)
print([Link]()) # removes whitespace from both ends
print([Link]('a','@')) # "@lice" — replace substrings
print("alice" in name) # True — substring check

1.3.2 int — Integer (Whole Numbers)


Integers are the counting numbers you learned in school: ..., -3, -2, -1, 0, 1, 2, 3, ... They have
no decimal point. Python's integers have no size limit — they can be as large as your
computer's memory allows, which is a luxury not all languages provide.

score = 100
temperature = -5
students = 0

# Integer arithmetic operators


print(score + 50) # 150 — addition
print(score - 30) # 70 — subtraction
print(score * 2) # 200 — multiplication
print(score / 3) # 33.333... — true division (returns float!)
print(score // 3) # 33 — integer (floor) division
print(score % 3) # 1 — modulo: remainder after division
print(score ** 2) # 10000 — exponentiation (100 squared)

The modulo operator (%) deserves special attention. It gives you the remainder of a division.
If you have 100 candies and 3 children, each child gets 33 candies (100 // 3 = 33), and 1 candy
remains (100 % 3 = 1). Modulo is used everywhere: checking if a number is even (n % 2 == 0),
cycling through a list, implementing clocks and calendars.

—8—
CS50 Python — A Complete Course Companion CS50 Python

1.3.3 float — Floating Point (Decimal Numbers)


Floats are numbers with a decimal point. The word 'float' refers to the floating decimal — it can
appear anywhere in the number. Under the hood, Python uses IEEE 754 double-precision
binary format to store floats, which has a famous quirk:

price = 9.99
pi = 3.14159

print(0.1 + 0.2) # Expected: 0.3


# Actual: 0.30000000000000004

print(round(0.1+0.2, 2)) # 0.3 — use round() for display

Why The result 0.30000000000000004 is not a Python bug. Computers store


0.1 + numbers in binary (base 2), and 0.1 cannot be represented exactly in binary
0.2 ≠ — just as 1/3 cannot be written exactly in decimal (0.333...). This is a

0.3 fundamental limitation of floating-point arithmetic. Always use round() when


displaying financial or precise values.

1.3.4 bool — Boolean (True or False)


A boolean is the simplest possible data type: it is either True or False. Nothing in between.
Named after mathematician George Boole who formalized logic in the 1840s, booleans are the
foundation of every decision your program makes.

is_raining = True
has_passed_exam = False

# Booleans from comparisons


print(5 > 3) # True
print(5 == 5) # True (== means 'is equal to')
print(5 != 3) # True (!= means 'not equal to')
print(10 < 5) # False
print(type(True)) # <class 'bool'>

—9—
CS50 Python — A Complete Course Companion CS50 Python

Com = and == are completely different. A single = means assignment: "store this
mon value in this variable." Double == means comparison: "are these two values
Mista equal?" Writing if x = 5 is a SyntaxError. You want if x == 5.

ke

1.3.5 NoneType — The Absence of Value


None represents the absence of a value. Think of it as an empty box that has a label but no
contents. It is Python's way of saying 'nothing is here yet' or 'this function doesn't return
anything meaningful.' Every function that lacks an explicit return statement implicitly returns
None.

result = None
print(result) # None
print(type(result)) # <class 'NoneType'>

# Correct way to check for None


if result is None:
print("No result yet.")

1.4 Type Checking and Type Conversion


You can always ask Python what type a variable holds using the type() function. And you can
convert between types using conversion functions — but only when the conversion makes
logical sense.

name = "Alice"
age = 21
height = 5.7
active = True

print(type(name)) # <class 'str'>


print(type(age)) # <class 'int'>
print(type(height)) # <class 'float'>
print(type(active)) # <class 'bool'>

Type Conversion Functions

— 10 —
CS50 Python — A Complete Course Companion CS50 Python

# str -> int


age_text = "21"
age_number = int(age_text) # 21 (integer)
print(age_number + 1) # 22

# int -> str (needed to concatenate with text)


score = 95
message = "Your score: " + str(score)
print(message) # 'Your score: 95'

# int -> float


whole = 5
decimal = float(whole) # 5.0

# float -> int (TRUNCATES, does not round!)


pi = 3.99
whole_pi = int(pi) # 3 (the .99 is simply cut off)

# str -> float


price = float("9.99") # 9.99

Com int() does not round — it truncates. int(9.9) gives 9, not 10. int(3.1) gives 3.
mon If you want rounding, use round() first: int(round(9.9)) gives 10.
Mista
ke

1.5 Getting Input from the User


The input() function pauses your program, displays a prompt, waits for the user to type
something and press Enter, and returns whatever they typed as a string. Always a string. No
exceptions.

name = input("What is your name? ")


print(f"Hello, {name}!")

# IMPORTANT: input() always returns a string


# You MUST convert if you need a number:
age = int(input("How old are you? "))

— 11 —
CS50 Python — A Complete Course Companion CS50 Python

print(age + 1) # Works correctly now

input("How old are you? ")


|
1. Prints the prompt: How old are you?
2. Program pauses and waits
3. User types: 25 (and presses Enter)
4. Returns the string "25"
|
int("25") --> 25 (integer)

Com The most common beginner error with input(): forgetting it always returns a
mon string. age = input('Age: ') gives you the string '25', not the number 25.
Mista Writing age > 18 then causes a TypeError because Python cannot compare

ke a string to a number. Always wrap with int() or float().

1.6 Reassignment and Compound Operators

# A variable can be reassigned at any time


score = 50
score = 75 # 50 is gone; score now points to 75
score = score + 10 # Read current value, add 10, store back
print(score) # 85

# Shorthand compound operators


score += 10 # same as: score = score + 10
score -= 5 # same as: score = score - 5
score *= 2 # same as: score = score * 2
score //= 3 # same as: score = score // 3

# Multiple assignment in one line


x, y, z = 1, 2, 3

# Swapping values (no temp variable needed in Python)


a, b = 5, 10
a, b = b, a
print(a, b) # 10 5

— 12 —
CS50 Python — A Complete Course Companion CS50 Python

1.7 Common Mistakes and How to Avoid Them

# Mistake 1: Using = instead of == in a condition


if score = 100: # SyntaxError!
if score == 100: # Correct

# Mistake 2: Forgetting to convert input()


age = input("Age: ")
if age > 18: # TypeError: '>' not supported between str and int
age = int(input("Age: ")) # Correct

# Mistake 3: int() rounds (it doesn't — it truncates)


int(9.9) # Returns 9, not 10

# Mistake 4: Using a variable before assigning it


print(score) # NameError: name 'score' is not defined
score = 100 # Always assign before use

# Mistake 5: Case sensitivity surprise


Name = "Alice"
print(name) # NameError — 'name' and 'Name' are different!

# Mistake 6: Checking None with ==


if result == None: # Works but not Pythonic
if result is None: # Correct Pythonic style

Practice Problems

Easy
1. Ask the user for their name and age. Print: "Hello [name], you are [age] years old."
2. Create two integers a = 17 and b = 5. Print their sum, difference, product, integer
quotient, and remainder.
3. Convert the string "3.14" to a float and multiply by 2. Print the result.

Medium
1. Ask the user for a temperature in Celsius. Convert to Fahrenheit using F = (C * 9/5) + 32.
Display the result rounded to 1 decimal place.

— 13 —
CS50 Python — A Complete Course Companion CS50 Python

2. Ask the user for their first and last name separately. Combine them and print the full
name with proper capitalization regardless of how the user typed it.
3. Ask the user for a number of seconds. Convert and display it as hours, minutes, and
seconds (e.g., 3661 seconds = 1 hour, 1 minute, 1 second).

Challenging
1. Ask the user to enter two numbers. Without any if statement, compute and print: their
sum, their average as a float, whether the first is larger (a boolean), and the remainder of
dividing the first by the second.
2. Write a program that asks for a 10-digit phone number as a string and reformats it to
(XXX) XXX-XXXX format using string slicing.
3. Ask the user for a sentence. Print: the sentence in uppercase, the number of characters
(including spaces), the number of words, and whether the sentence ends with a question
mark.

Solutions

# Easy 1
name = input("What is your name? ")
age = input("How old are you? ")
print(f"Hello {name}, you are {age} years old.")

# Easy 2
a, b = 17, 5
print(f"Sum: {a+b}, Diff: {a-b}, Product: {a*b}")
print(f"Quotient: {a//b}, Remainder: {a%b}")

# Easy 3
result = float("3.14") * 2
print(result) # 6.28

# Medium 1
celsius = float(input("Temperature in Celsius: "))
fahrenheit = (celsius * 9/5) + 32
print(f"{celsius}C = {round(fahrenheit, 1)}F")

# Medium 3
total_secs = int(input("Enter seconds: "))
hours = total_secs // 3600

— 14 —
CS50 Python — A Complete Course Companion CS50 Python

remainder = total_secs % 3600


minutes = remainder // 60
seconds = remainder % 60
print(f"{hours}h {minutes}m {seconds}s")

# Hard 2
phone = input("Enter 10-digit phone number: ")
formatted = f"({phone[:3]}) {phone[3:6]}-{phone[6:]}"
print(formatted)

# Hard 3
sentence = input("Enter a sentence: ")
print([Link]())
print(f"Characters: {len(sentence)}")
print(f"Words: {len([Link]())}")
print(f"Ends with ?: {[Link]('?')}")

Chapter Summary

• A variable is a named reference to a value stored in memory, created with the =


operator.

• Python variables use reference semantics — they point to objects, not contain them.

• Use snake_case names that are descriptive and meaningful.

• Five core types: str (text), int (whole numbers), float (decimals), bool (True/False),
NoneType (no value).

• input() always returns a string — convert with int() or float() when you need
numbers.

• int() truncates (cuts off) the decimal part; it does not round.

• Use is None (not == None) to check for the absence of a value.

• Compound operators (+=, -=, *=) provide shorthand for common reassignment
patterns.

— 15 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 2

Conditionals

"A program without conditionals is just a recipe — it does the same thing every
time. Conditionals are what give programs the ability to think."

2.1 Why Conditionals Exist

Analo Imagine a theme park entrance. The guard checks your height against a
gy sign: 'You must be at least 120 cm to ride.' If you are tall enough — you go
in. If not — you don't. That moment of checking and choosing a path is
exactly what a conditional does in code.

Without conditionals, every run of your program would produce identical output regardless of
the input. Conditionals let your program decide — and that decision-making capability is the
foundation of everything useful that computers do.

2.2 The if Statement


The simplest conditional checks one condition. If it is true, the indented block runs. If it is false,
that block is silently skipped.

age = 18

if age >= 18:


print("You may vote.")

Anatomy of an if Statement

if age >= 18 :
| | | | |
| | | | Colon: MANDATORY. Signals block start.
| | | Value to compare against
| | Comparison operator (greater than or equal to)
| Variable being checked

— 16 —
CS50 Python — A Complete Course Companion CS50 Python

Keyword that begins the conditional

print("You may vote.")


^^^^
4 spaces of indentation — Python uses this to define the block

Python uses indentation (whitespace at the start of a line) to define blocks of code. This is not
a style choice — it is mandatory syntax. Every line inside an if block must be indented by the
same amount (conventionally 4 spaces).

Execution Trace

age = 15
if age >= 18: Step 1: Evaluate 'age >= 18'
print("Vote") Step 2: 15 >= 18 is False
Step 3: Skip the block entirely
print("Done") Step 4: This runs regardless

2.3 if / else — Two Paths


An else clause provides the fallback — the code that runs when the condition is false. Exactly
one of the two branches always runs.

age = 15

if age >= 18:


print("You may vote.")
else:
print("You are too young to vote.")

The else block has no condition of its own. It is the catch-all that handles every case the if did
not. Together, if and else form a complete binary fork: one path or the other, always exactly
one.

2.4 elif — Multiple Conditions in a Chain

— 17 —
CS50 Python — A Complete Course Companion CS50 Python

Analo A cinema has different ticket prices: under 5 is free, 5-17 is a child ticket,
gy 18-64 is adult, 65+ is senior. You cannot express four outcomes with just if
and else. You need elif — 'else if' — to chain multiple conditions.

age = int(input("Enter your age: "))

if age < 5:
print("Free entry!")
elif age < 18:
print("Child ticket: $6")
elif age < 65:
print("Adult ticket: $12")
else:
print("Senior ticket: $8")

The Critical Rule: Python Stops at the First Match


This is the most important and most misunderstood aspect of elif chains. Python checks
conditions from top to bottom and stops the moment one is True. It never evaluates the
remaining conditions.

age = 10

Check 1: Is 10 < 5? No -> skip


Check 2: Is 10 < 18? YES -> run this block -> 'Child ticket: $6'
STOP. Python exits the chain.
Check 3: (never reached)
else: (never reached)

Com Order matters enormously in elif chains. If you put a more general condition
mon before a specific one, the specific one may never be reached. Always put
Mista the MOST SPECIFIC condition first. Example: if score >= 90 must come

ke before if score >= 60, or every score of 90+ would match the second
condition and never reach the first.

2.5 Comparison Operators

— 18 —
CS50 Python — A Complete Course Companion CS50 Python

Operator Meaning Example Result

== Equal to 5 == 5 True

!= Not equal to 5 != 3 True

> Greater than 10 > 5 True

< Less than 3<1 False

>= Greater than or equal 5 >= 5 True

<= Less than or equal 4 <= 3 False

2.6 Logical Operators — Combining Conditions


Three operators let you combine multiple conditions into one: and, or, and not.

and — Both Must Be True

age = 25
has_id = True

if age >= 18 and has_id:


print("Welcome to the club.")

# Truth table for 'and':


# True and True -> True
# True and False -> False
# False and True -> False
# False and False -> False

or — At Least One Must Be True

is_student = True
is_senior = False

if is_student or is_senior:
print("Discount applied.")

# Truth table for 'or':


# True or True -> True

— 19 —
CS50 Python — A Complete Course Companion CS50 Python

# True or False -> True


# False or True -> True
# False or False -> False

not — Reverses the Truth Value

is_raining = False

if not is_raining:
print("Good weather for a walk!")

# not True -> False


# not False -> True

Combining Operators — Use Parentheses for Clarity

age = 20
is_vip = False
has_invitation = True

if age >= 18 and (is_vip or has_invitation):


print("Access granted.")

# Evaluation order (without parentheses, 'and' binds tighter than 'or')


# a and b or c is read as (a and b) or c
# Use parentheses to be explicit — it costs nothing and prevents bugs.

2.7 Pythonic Conditionals


Python encourages writing conditions in a natural, readable way. Here are patterns every
Python programmer should know.

# Checking booleans — verbose vs Pythonic


if is_logged_in == True: # Verbose (not wrong, just wordy)
pass
if is_logged_in: # Pythonic
pass
if not is_logged_in: # Pythonic negative check
pass

— 20 —
CS50 Python — A Complete Course Companion CS50 Python

# Chained comparisons (Python's elegant syntax)


score = 85
if 60 <= score <= 100: # Equivalent to: score >= 60 and score <= 100
print("Valid score")

# The 'in' operator for membership checking


grade = "B"
if grade in ["A", "B", "C"]:
print("Passing grade")

# Ternary (conditional) expression — one-liner


status = "adult" if age >= 18 else "minor"
print(status)

2.8 The match Statement (Python 3.10+)


The match statement, introduced in Python 3.10, provides clean multi-way branching when
comparing one variable against many specific values. It is similar to 'switch' statements in other
languages but significantly more powerful.

language = input("Which language? ").lower()

match language:
case "python":
print("Great choice for beginners!")
case "javascript":
print("The language of the web!")
case "c" | "c++": # The | means OR between cases
print("You like living dangerously.")
case _: # _ is the wildcard (like else)
print("I don't know that language.")

2.9 Nested Conditionals

score = int(input("Enter score: "))

if score >= 60:


print("You passed!")

— 21 —
CS50 Python — A Complete Course Companion CS50 Python

if score >= 90:


print("With distinction!")
elif score >= 75:
print("With merit!")
else:
print("You failed.")

Nested conditionals are appropriate when a second decision only makes sense after the first
one has already been resolved. However, avoid nesting more than 2-3 levels deep — deeply
nested code is difficult to read and debug. Often, deep nesting can be replaced with logical
operators or early returns.

2.10 Full Example — Grade Calculator

score = int(input("Enter exam score (0-100): "))

if score < 0 or score > 100:


print("Invalid score. Enter a value between 0 and 100.")
elif score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
elif score >= 60:
grade = "D"
else:
grade = "F"

if 0 <= score <= 100:


print(f"Score: {score} | Grade: {grade}")
if grade == "A":
print("Outstanding!")
elif grade in ["B", "C"]:
print("Good work. Keep it up.")
else:
print("Consider visiting office hours.")

2.11 Common Mistakes

— 22 —
CS50 Python — A Complete Course Companion CS50 Python

# Mistake 1: = instead of == in condition


if score = 100: # SyntaxError!
if score == 100: # Correct

# Mistake 2: Missing colon


if score >= 60 # SyntaxError: expected ':'
if score >= 60: # Correct

# Mistake 3: Wrong indentation


if score >= 60:
print("Passed") # IndentationError
if score >= 60:
print("Passed") # Correct

# Mistake 4: Order of elif conditions


if score >= 60:
grade = "D"
elif score >= 90: # Never reached! 90 already caught by >= 60
grade = "A"

# Mistake 5: Comparing strings without case normalization


answer = input("Yes or no? ")
if answer == "yes": # Fails for 'Yes', 'YES', 'yEs'
if [Link]() == "yes": # Robust

Practice Problems

Easy
1. Ask for a number. Print 'positive', 'negative', or 'zero'.
2. Ask for a username and password. If username is 'admin' and password is 'cs50', print
'Access granted.' Otherwise print 'Access denied.'
3. Ask for a temperature in Celsius. Print 'Freezing' (below 0), 'Cold' (0-20), 'Warm' (21-35),
or 'Hot' (above 35).

Medium
1. Ask for two numbers and an operator (+, -, *, /). Perform the calculation. Handle division
by zero gracefully.

— 23 —
CS50 Python — A Complete Course Companion CS50 Python

2. A cinema charges different prices by age group (see Section 2.4). If it is Tuesday, apply a
20% discount to all paying customers. Ask for age and day, print the final price.
3. Ask the user for a year. Determine if it is a leap year. (Leap year rules: divisible by 4,
except centuries unless also divisible by 400.)

Challenging
1. Build a simple login with exactly 3 attempts. If the correct password ('python50') is
entered, print 'Welcome!' and stop. After 3 failures, print 'Account locked.'
2. Ask for three side lengths. Determine if they form a valid triangle. If valid, classify it as
equilateral, isosceles, or scalene.
3. Create a BMI calculator. Ask for weight (kg) and height (m). Compute BMI. Classify as
Underweight (<18.5), Normal (18.5-24.9), Overweight (25-29.9), or Obese (30+). Print the
classification and a one-sentence health note for each category.

Solutions

# Easy 1
n = float(input("Enter a number: "))
if n > 0: print("Positive")
elif n < 0: print("Negative")
else: print("Zero")

# Easy 3
temp = float(input("Temperature: "))
if temp < 0: print("Freezing")
elif temp <= 20: print("Cold")
elif temp <= 35: print("Warm")
else: print("Hot")

# Medium 1
a = float(input("First number: "))
op = input("Operator: ")
b = float(input("Second number: "))
if op == "+": print(a + b)
elif op == "-": print(a - b)
elif op == "*": print(a * b)
elif op == "/":
if b == 0: print("Error: Division by zero")
else: print(a / b)

— 24 —
CS50 Python — A Complete Course Companion CS50 Python

else: print("Unknown operator")

# Medium 3 — Leap year


year = int(input("Enter year: "))
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
print(f"{year} is a leap year.")
else:
print(f"{year} is not a leap year.")

# Hard 2 — Triangle
a = float(input("Side 1: "))
b = float(input("Side 2: "))
c = float(input("Side 3: "))
if a + b > c and a + c > b and b + c > a:
if a == b == c: print("Equilateral")
elif a == b or b == c or a == c: print("Isosceles")
else: print("Scalene")
else:
print("Not a valid triangle.")

Chapter Summary

• if evaluates a condition; the indented block runs only when it is True.

• else provides the fallback — exactly one branch (if or else) always executes.

• elif chains multiple conditions; Python checks top-to-bottom and stops at the first
True.

• Put the most specific conditions first in an elif chain.

• Comparison operators: ==, !=, >, <, >=, <= — always return a boolean.

• Logical operators: and (both must be True), or (at least one), not (reverses).

• Pythonic conditionals: use if x: instead of if x == True:, and chained comparisons like


0 <= x <= 100.

• The match statement provides clean multi-value branching (Python 3.10+).

— 25 —
CS50 Python — A Complete Course Companion CS50 Python

• Indentation is mandatory syntax in Python, not optional style.

— 26 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 3

Loops

"If you find yourself writing the same lines of code three times, a loop is trying to
be born."

3.1 Why Loops Exist

Analo Imagine you're a factory worker stamping 10,000 envelopes. You don't write
gy 'stamp envelope' 10,000 times on your to-do list. You write 'stamp one
envelope' and repeat it until all 10,000 are done. That repetition is exactly
what a loop does.

Loops are one of the most powerful ideas in programming. They let you execute a block of
code repeatedly — either a fixed number of times, or until some condition changes. Without
loops, your code would grow linearly with your problem. With loops, a program of 10 lines can
process a million items.

3.2 The while Loop — Repeat While True


The while loop checks a condition before each iteration. As long as the condition remains
True, the loop body keeps executing. The moment the condition becomes False, the loop exits
and the program continues.

count = 1

while count <= 5:


print(f"Count is {count}")
count += 1 # CRITICAL: update the variable or loop forever

print("Loop finished.")

Execution Trace

count = 1

— 27 —
CS50 Python — A Complete Course Companion CS50 Python

|
Check: 1 <= 5? True -> print 'Count is 1' -> count = 2
Check: 2 <= 5? True -> print 'Count is 2' -> count = 3
Check: 3 <= 5? True -> print 'Count is 3' -> count = 4
Check: 4 <= 5? True -> print 'Count is 4' -> count = 5
Check: 5 <= 5? True -> print 'Count is 5' -> count = 6
Check: 6 <= 5? False -> EXIT LOOP
|
print 'Loop finished.'

Com Infinite loops: if you forget to update the loop variable (count += 1), the
mon condition never becomes False and the loop runs forever — crashing your
Mista program. Always make sure something inside the loop makes progress

ke toward the exit condition.

while with User Input — The Validation Pattern


One of the most common and practical uses of while is to keep asking the user for input until
they provide something valid.

while True: # Loop forever initially


age = int(input("Enter your age: "))
if age > 0 and age < 150:
break # Exit only on valid input
print("Please enter a realistic age.")

print(f"Your age is {age}.")

3.3 The for Loop — Iterate Over a Sequence


The for loop is Python's most commonly used loop. It iterates over any sequence — a list, a
string, a range of numbers — automatically handling the counter and stopping condition for you.

# Looping over a list


fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
print(fruit)

# Output:

— 28 —
CS50 Python — A Complete Course Companion CS50 Python

# apple
# banana
# cherry

Word-by-Word Breakdown

for fruit in fruits :


| | | | |
| | | | Colon: block follows
| | | The sequence to iterate over
| | Keyword: 'from each item in'
| Loop variable (gets a new value each iteration)
Keyword that begins a for loop

The loop variable (fruit) is automatically assigned the next item from the sequence at the
start of each iteration. You do not manage an index or counter — Python does it for you.

3.4 range() — Generating Number Sequences


The range() function generates a sequence of integers. It is the primary tool for running a
loop a specific number of times.

# range(stop) — 0 to stop-1
for i in range(5):
print(i) # 0, 1, 2, 3, 4

# range(start, stop) — start to stop-1


for i in range(2, 7):
print(i) # 2, 3, 4, 5, 6

# range(start, stop, step) — with a step size


for i in range(0, 20, 5):
print(i) # 0, 5, 10, 15

# Counting down
for i in range(10, 0, -1):
print(i) # 10, 9, 8, 7, ... 1

— 29 —
CS50 Python — A Complete Course Companion CS50 Python

range range() is 'stop-exclusive' — the stop value is NEVER included. range(5)


() Is S gives 0,1,2,3,4 (five numbers). range(1,6) gives 1,2,3,4,5. This is consistent
top-E with how Python handles all sequences and slicing.

xclusi
ve

Looping Over Strings

word = "Python"
for letter in word:
print(letter)

# Output: P y t h o n (each on its own line)

# Count vowels
vowels = 0
for letter in word:
if [Link]() in "aeiou":
vowels += 1
print(f"Vowels: {vowels}")

enumerate() — Index and Value Together


When you need both the index (position) and the value, use enumerate() instead of manually
managing a counter.

fruits = ["apple", "banana", "cherry"]

# Without enumerate (awkward)


i = 0
for fruit in fruits:
print(f"{i}: {fruit}")
i += 1

# With enumerate (Pythonic and clean)


for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")

# Output:

— 30 —
CS50 Python — A Complete Course Companion CS50 Python

# 0: apple
# 1: banana
# 2: cherry

3.5 Loop Control — break, continue, pass

break — Exit the Loop Immediately


break exits the entire loop immediately, regardless of whether the loop condition is still True or
items remain in the sequence.

# Find the first even number in a list


numbers = [1, 3, 7, 4, 9, 2, 6]

for n in numbers:
if n % 2 == 0:
print(f"First even: {n}")
break # Stop as soon as we find it

# Output: First even: 4

continue — Skip This Iteration


continue skips the rest of the current iteration's code and jumps directly to the next iteration
of the loop.

# Print only odd numbers


for n in range(1, 11):
if n % 2 == 0:
continue # Skip even numbers
print(n) # Only reaches here for odd n

# Output: 1 3 5 7 9

pass — Do Nothing (Placeholder)


pass is a no-operation statement. It exists because Python requires at least one statement in
every block. Use it as a placeholder when you want to write a block you will fill in later.

for i in range(5):

— 31 —
CS50 Python — A Complete Course Companion CS50 Python

pass # Loop runs 5 times but does nothing

# Common in class and function stubs:


def my_future_function():
pass # TODO: implement this

3.6 Nested Loops


A loop inside another loop is called a nested loop. The inner loop runs completely for every
single iteration of the outer loop.

# Multiplication table (partial)


for row in range(1, 4):
for col in range(1, 4):
print(f"{row * col:3}", end="")
print() # New line after each row

# Output:
# 1 2 3
# 2 4 6
# 3 6 9

Outer loop iteration 1 (row=1):


Inner loop: col=1 -> print 1
Inner loop: col=2 -> print 2
Inner loop: col=3 -> print 3
newline
Outer loop iteration 2 (row=2):
Inner loop: col=1 -> print 2
... (and so on)

Warning: nested loops can become slow quickly. A loop inside a loop runs O(n²) operations.
Three nested loops run O(n³). For large data, look for alternative approaches.

3.7 Common Loop Patterns

# Pattern 1: Accumulator — summing values


total = 0
numbers = [10, 20, 30, 40, 50]

— 32 —
CS50 Python — A Complete Course Companion CS50 Python

for n in numbers:
total += n
print(f"Sum: {total}") # 150

# Pattern 2: Counter — counting occurrences


sentence = "hello world"
count = 0
for char in sentence:
if char == 'l':
count += 1
print(f"'l' appears {count} times") # 3

# Pattern 3: Finding maximum


scores = [72, 95, 61, 88, 95, 74]
maximum = scores[0]
for score in scores:
if score > maximum:
maximum = score
print(f"Highest score: {maximum}") # 95

# Pattern 4: Building a result list


numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
[Link](n ** 2)
print(squares) # [1, 4, 9, 16, 25]

# Pattern 5: List comprehension (Pythonic shorthand)


squares = [n ** 2 for n in range(1, 6)]
print(squares) # [1, 4, 9, 16, 25]

3.8 Common Mistakes

# Mistake 1: Modifying a list while iterating over it


numbers = [1, 2, 3, 4, 5]
for n in numbers:
if n % 2 == 0:
[Link](n) # DANGEROUS — unpredictable results
# Fix: iterate over a copy
for n in numbers[:]: # numbers[:] creates a copy

— 33 —
CS50 Python — A Complete Course Companion CS50 Python

if n % 2 == 0:
[Link](n)

# Mistake 2: Off-by-one with range


for i in range(5): # gives 0,1,2,3,4 (not 1-5!)
for i in range(1, 6): # gives 1,2,3,4,5 — if that's what you want

# Mistake 3: Using = instead of == in the loop condition


count = 0
while count = 5: # SyntaxError!
while count == 5: # Correct comparison

# Mistake 4: Forgetting to update in while loop


count = 0
while count < 5:
print(count) # Infinite loop! count never changes

while count < 5: # Correct


print(count)
count += 1

Practice Problems

Easy
1. Use a for loop to print the numbers 1 through 20.
2. Use a while loop to ask the user for a positive number. Keep asking until they comply.
3. Loop through the string 'Mississippi' and count how many times the letter 's' appears.

Medium
1. Print a right-angled triangle of asterisks with n rows (ask user for n). Row 1 has 1 star,
row 2 has 2, etc.
2. Write a program that finds all prime numbers between 2 and 100 using nested loops.
3. Ask the user for 5 exam scores. Calculate and display the average, highest, and lowest
score.

Challenging

— 34 —
CS50 Python — A Complete Course Companion CS50 Python

1. Write a number guessing game: generate a random number 1-100 (use [Link]).
Let the user guess repeatedly. Tell them 'too high' or 'too low' after each guess. Count and
display the number of guesses when they get it right.
2. Create a program that prints a diamond shape of asterisks. Ask the user for the half-width
n. (e.g., n=3 produces a diamond 5 rows tall.)
3. Given a list of words, use loops to build a dictionary counting how many times each
unique word appears. Print the results sorted by count (most common first).

Solutions

# Easy 1
for i in range(1, 21):
print(i)

# Easy 3
count = 0
for char in 'Mississippi':
if char == 's':
count += 1
print(f"s appears {count} times")

# Medium 1 — Triangle
n = int(input("Number of rows: "))
for i in range(1, n + 1):
print('*' * i)

# Medium 2 — Primes
for n in range(2, 101):
is_prime = True
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
is_prime = False
break
if is_prime:
print(n)

# Hard 1 — Guessing game


import random
secret = [Link](1, 100)
guesses = 0

— 35 —
CS50 Python — A Complete Course Companion CS50 Python

while True:
guess = int(input("Guess (1-100): "))
guesses += 1
if guess < secret: print("Too low!")
elif guess > secret: print("Too high!")
else:
print(f"Correct in {guesses} guesses!")
break

Chapter Summary

• while loops repeat as long as a condition is True — always ensure something


changes to avoid infinite loops.

• for loops iterate over any sequence (list, string, range) — Python handles the
counter automatically.

• range(stop), range(start, stop), and range(start, stop, step) generate integer


sequences.

• range() is stop-exclusive: range(5) gives 0,1,2,3,4.

• break exits the entire loop; continue skips to the next iteration; pass does nothing.

• enumerate() gives you both index and value in a for loop.

• Nested loops run in O(n²) time — use carefully with large data.

• Common patterns: accumulator, counter, max/min finder, list builder.

• List comprehensions ([x**2 for x in range(5)]) are the Pythonic way to build lists from
loops.

— 36 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 4

Functions

"A function is a promise: give me this input, and I will give you back that output.
Every time."

4.1 Why Functions Exist

Analo Imagine a coffee machine. You don't need to know how it heats water,
gy grinds beans, and pressurizes steam. You press a button and get espresso.
A function is that button — it hides complexity behind a simple interface.
Press the button, get the result.

Functions serve three essential purposes in programming:

1. Reusability — Write code once, call it a thousand times without rewriting it.
2. Abstraction — Hide implementation details. Other code only needs to know what a
function does, not how it does it.
3. Organization — Break large problems into small, testable, independently-working
pieces.

4.2 Defining Your First Function

def greet(name):
"""Print a personalized greeting."""
message = f"Hello, {name}! Welcome to Python."
print(message)

# Calling the function


greet("Alice") # Hello, Alice! Welcome to Python.
greet("Bob") # Hello, Bob! Welcome to Python.

Anatomy of a Function Definition

def greet (name) :

— 37 —
CS50 Python — A Complete Course Companion CS50 Python

| | | |
| | | Colon: block follows
| | Parameter: input the function expects
| Function name (snake_case by convention)
Keyword 'define': tells Python a function is being created

"""Print a personalized greeting."""


|
Docstring: describes what the function does.
Always include one. It becomes the function's help text.

message = f"Hello, {name}! Welcome to Python."


print(message)
|
The function body: indented code that runs when called

Defining a function does NOT run it. It only teaches Python what to do when the function is
called. The function body runs only when you explicitly call the function with its name followed
by parentheses.

4.3 Parameters and Arguments


A parameter is the variable name listed in the function definition — it is a placeholder. An
argument is the actual value passed when calling the function. The parameter receives the
argument's value for the duration of that function call.

def add(a, b): # 'a' and 'b' are PARAMETERS


return a + b

result = add(3, 7) # 3 and 7 are ARGUMENTS


print(result) # 10

# Multiple parameters
def describe_pet(name, species, age):
print(f"{name} is a {age}-year-old {species}.")

describe_pet("Buddy", "dog", 3)
# Output: Buddy is a 3-year-old dog.

— 38 —
CS50 Python — A Complete Course Companion CS50 Python

4.4 The return Statement


The return statement does two things: it sends a value back to whoever called the function,
and it immediately exits the function. A function without a return statement implicitly returns
None.

def square(n):
return n * n # Sends n*n back to the caller

result = square(5) # result = 25


print(result + 1) # 26 — we can use the returned value
print(square(4) * 2) # 32 — use return value directly

# A function can return multiple values (as a tuple)


def min_max(numbers):
return min(numbers), max(numbers)

low, high = min_max([3, 1, 4, 1, 5, 9, 2, 6])


print(f"Min: {low}, Max: {high}") # Min: 1, Max: 9

Early Return — Using return to Exit Early

def is_valid_age(age):
if age < 0:
return False # Exit immediately for negative ages
if age > 150:
return False # Exit immediately for unrealistic ages
return True # Only reached if both checks passed

print(is_valid_age(25)) # True
print(is_valid_age(-1)) # False
print(is_valid_age(200)) # False

4.5 Scope — Where Variables Live

— 39 —
CS50 Python — A Complete Course Companion CS50 Python

Analo Think of each function as a separate room in a house. Variables created


gy inside a room exist only in that room. You can't see them from the hallway,
and they disappear when you leave the room. The hallway (global scope)
can be seen from any room, but modifying it from inside a room requires
special permission.

x = 10 # Global variable — visible everywhere

def my_function():
y = 20 # Local variable — visible only inside my_function
print(x) # 10 — can READ global variables
print(y) # 20

my_function()
print(x) # 10 — still works
print(y) # NameError! y doesn't exist outside the function

# To MODIFY a global variable inside a function, declare global:


count = 0
def increment():
global count # Without this, assignment creates a LOCAL count
count += 1

increment()
print(count) # 1

Globa Avoid using global variables whenever possible. They make code hard to
l Vari understand and test because any function can change them at any time.
ables Prefer passing values as arguments and returning results — this is the

Are U foundation of well-designed code.

sually
a Des
ign
Smell

4.6 Default Arguments

— 40 —
CS50 Python — A Complete Course Companion CS50 Python

A parameter can have a default value. If the caller doesn't provide that argument, the default is
used. This makes functions more flexible without requiring every argument to be specified on
every call.

def greet(name, greeting="Hello"):


print(f"{greeting}, {name}!")

greet("Alice") # Hello, Alice! (uses default)


greet("Bob", "Good morning") # Good morning, Bob! (overrides default)

# Keyword arguments — call with argument names explicitly


def create_user(name, age, role="member"):
print(f"User: {name}, Age: {age}, Role: {role}")

create_user("Alice", 30)
create_user(age=25, name="Bob", role="admin") # order doesn't matter

Com Default arguments must come AFTER non-default arguments in the


mon definition. def greet(greeting='Hello', name) is a SyntaxError because a
Mista non-default parameter follows a default one. Always put defaults last.

ke

4.7 Variable Arguments — *args and **kwargs


Sometimes you don't know in advance how many arguments a function will receive. Python
provides two special syntaxes to handle this.

# *args: accept any number of positional arguments (as a tuple)


def add_all(*numbers):
total = 0
for n in numbers:
total += n
return total

print(add_all(1, 2, 3)) # 6
print(add_all(10, 20, 30, 40)) # 100

# **kwargs: accept any number of keyword arguments (as a dict)


def print_profile(**info):

— 41 —
CS50 Python — A Complete Course Companion CS50 Python

for key, value in [Link]():


print(f" {key}: {value}")

print_profile(name="Alice", age=30, city="Lahore")


# name: Alice
# age: 30
# city: Lahore

4.8 Lambda Functions


A lambda is a small, anonymous (unnamed) function defined in a single expression. Use them
for short, throwaway operations — especially when passing a function as an argument to
another function.

# Regular function
def square(n):
return n * n

# Equivalent lambda
square = lambda n: n * n
print(square(5)) # 25

# Most common use: with sorted(), map(), filter()


names = ["Charlie", "Alice", "Bob"]
[Link](key=lambda name: len(name)) # Sort by length
print(names) # ['Bob', 'Alice', 'Charlie']

numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, numbers))
print(evens) # [2, 4, 6]

4.9 Recursion — Functions Calling Themselves

Analo Russian matryoshka dolls: you open a doll and find a smaller doll inside.
gy You open that one and find an even smaller one. At some point you reach
the smallest doll — the one that doesn't open. That's the base case.
Recursion is a function solving a problem by solving a smaller version of
itself, until it reaches a case small enough to solve directly.

— 42 —
CS50 Python — A Complete Course Companion CS50 Python

# Factorial: n! = n * (n-1) * (n-2) * ... * 1


def factorial(n):
if n == 0 or n == 1: # Base case: stop here
return 1
return n * factorial(n - 1) # Recursive case

print(factorial(5)) # 120

# Execution trace for factorial(4):


# factorial(4) = 4 * factorial(3)
# factorial(3) = 3 * factorial(2)
# factorial(2) = 2 * factorial(1)
# factorial(1) = 1 (base case)
# Now: factorial(2) = 2 * 1 = 2
# factorial(3) = 3 * 2 = 6
# factorial(4) = 4 * 6 = 24

The Every recursive function MUST have a base case — a condition that stops
Base the recursion. Without it, the function calls itself forever until Python hits its
Case recursion limit (default: 1000 calls) and raises a RecursionError.

Is Not
Optio
nal

4.10 Common Mistakes

# Mistake 1: Forgetting return (function returns None silently)


def add(a, b):
a + b # Computes but doesn't return!
result = add(3, 7)
print(result) # None (unexpected!)

# Fix:
def add(a, b):
return a + b

# Mistake 2: Using a variable before it's defined in scope


def my_func():

— 43 —
CS50 Python — A Complete Course Companion CS50 Python

print(x) # UnboundLocalError if x is assigned later


x = 10

# Mistake 3: Mutable default argument (notorious Python gotcha)


def append_item(item, lst=[]): # DO NOT DO THIS
[Link](item)
return lst
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] -- list persists between calls!

# Fix: use None as default and create inside function


def append_item(item, lst=None):
if lst is None:
lst = []
[Link](item)
return lst

Practice Problems

Easy
1. Write a function is_even(n) that returns True if n is even, False otherwise.
2. Write a function celsius_to_fahrenheit(c) that converts Celsius to Fahrenheit.
3. Write a function count_vowels(s) that counts and returns the number of vowels in a string.

Medium
1. Write a function is_palindrome(s) that returns True if a string reads the same forwards
and backwards (case-insensitive).
2. Write a function fibonacci(n) that returns the n-th Fibonacci number using recursion.
3. Write a function calculator(a, b, op='+') that performs the specified operation and returns
the result. Handle division by zero.

Challenging
1. Write a function flatten(lst) that takes a nested list (lists inside lists of any depth) and
returns a single flat list using recursion. Example: flatten([1,[2,[3,4]],5]) -> [1,2,3,4,5].
2. Write a function memoize(func) that takes a function and returns a version of it that
caches its results so repeated calls with the same argument are instant.

— 44 —
CS50 Python — A Complete Course Companion CS50 Python

3. Write a function run_length_encode(s) that compresses a string using run-length


encoding. Example: 'aaabbbcc' -> '3a3b2c'.

Solutions

# Easy 1
def is_even(n):
return n % 2 == 0

# Easy 3
def count_vowels(s):
return sum(1 for char in s if [Link]() in 'aeiou')

# Medium 1
def is_palindrome(s):
s = [Link]()
return s == s[::-1]

# Medium 2
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)

# Hard 1 — flatten
def flatten(lst):
result = []
for item in lst:
if isinstance(item, list):
[Link](flatten(item))
else:
[Link](item)
return result

print(flatten([1, [2, [3, 4]], 5])) # [1, 2, 3, 4, 5]

# Hard 3 — run-length encoding


def run_length_encode(s):
if not s:
return ''

— 45 —
CS50 Python — A Complete Course Companion CS50 Python

result = ''
count = 1
for i in range(1, len(s)):
if s[i] == s[i-1]:
count += 1
else:
result += str(count) + s[i-1]
count = 1
result += str(count) + s[-1]
return result

print(run_length_encode('aaabbbcc')) # '3a3b2c'

Chapter Summary

• Functions are reusable blocks of code defined with def and called by name.

• Parameters are placeholders in the definition; arguments are the actual values
passed.

• return sends a value back to the caller and exits the function immediately.

• Local variables exist only inside their function; global variables exist everywhere.

• Default arguments make parameters optional: def func(x, y=0).

• *args collects any number of positional arguments into a tuple.

• **kwargs collects any number of keyword arguments into a dictionary.

• Lambda functions are anonymous one-line functions: lambda x: x*2.

• Recursive functions call themselves with a smaller problem and must have a base
case.

• Never use mutable objects (lists, dicts) as default argument values.

— 46 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 5

Exceptions

"The question is not whether your program will encounter errors. It will. The
question is whether your program handles them gracefully — or crashes and
leaves the user bewildered."

5.1 What Are Exceptions?

Analo Imagine a hotel concierge. A guest asks for a restaurant recommendation.


gy The concierge gives one. But what if the guest asks for a restaurant in a city
the concierge has never heard of? A bad concierge crashes — stares
blankly, breaks down. A good concierge says: 'I don't have that information,
but here's what I can do.' That graceful fallback is exception handling.

An exception is an event that disrupts the normal flow of a program. When Python encounters
something it cannot handle — dividing by zero, accessing a file that doesn't exist, converting
'hello' to an integer — it raises an exception. Without handling, the program crashes and prints
a traceback. With handling, you control what happens next.

Common Built-in Exceptions

Exception When It Occurs

ValueError Argument has right type but wrong value: int('abc')

TypeError Operation on wrong type: '3' + 3

ZeroDivisionError Division or modulo by zero: 10 / 0

NameError Variable used before assignment

IndexError List index out of range: lst[100]

KeyError Dictionary key not found: d['missing']

FileNotFoundError File doesn't exist: open('[Link]')

AttributeError Object lacks the attribute called

RecursionError Maximum recursion depth exceeded

— 47 —
CS50 Python — A Complete Course Companion CS50 Python

5.2 try / except — Catching Exceptions

try:
number = int(input("Enter a number: "))
result = 100 / number
print(f"100 / {number} = {result}")
except ValueError:
print("That wasn't a valid number!")
except ZeroDivisionError:
print("You can't divide by zero!")

How It Works

Python enters the try block and executes code normally.


If NO exception occurs: skip all except blocks, continue after.
If an exception occurs:
Python stops at the line that caused it.
Python looks for a matching except clause (top to bottom).
First matching except block runs.
Execution continues AFTER the try/except structure.

Catching Any Exception

try:
risky_code()
except Exception as e: # Catches any exception
print(f"Something went wrong: {e}")

# 'as e' binds the exception object to the name 'e'


# You can then inspect the error message with str(e)

Never Avoid bare 'except:' with no exception type — it catches everything


Use a including KeyboardInterrupt (Ctrl+C), which makes your program impossible
Bare to stop. At minimum, use 'except Exception:' which excludes system-exit

excep signals.

t:

— 48 —
CS50 Python — A Complete Course Companion CS50 Python

5.3 else and finally

try:
number = int(input("Enter a number: "))
result = 100 / number
except ValueError:
print("Invalid input.")
except ZeroDivisionError:
print("Cannot divide by zero.")
else:
# Runs ONLY if no exception occurred in try
print(f"Success! Result is {result}")
finally:
# ALWAYS runs — exception or not
print("This always executes.")

The else clause is the 'success path' — it only runs if the try block completed without raising
any exception. The finally clause runs no matter what — even if an exception was raised
and not caught, even if there's a return statement in the except block. Use finally for cleanup:
closing files, releasing resources.

5.4 Raising Exceptions


You can deliberately raise exceptions using the raise keyword. This is how you signal to the
caller that something went wrong in your function.

def set_age(age):
if not isinstance(age, int):
raise TypeError("Age must be an integer")
if age < 0 or age > 150:
raise ValueError(f"Age {age} is not realistic")
return age

try:
set_age(-5)
except ValueError as e:
print(f"Error: {e}")
# Output: Error: Age -5 is not realistic

— 49 —
CS50 Python — A Complete Course Companion CS50 Python

5.5 Custom Exception Classes


For larger programs, define your own exception types by creating classes that inherit from
Python's built-in Exception. This makes error handling more precise and expressive.

# Define custom exceptions


class InsufficientFundsError(Exception):
"""Raised when a bank account has insufficient funds."""
def __init__(self, amount, balance):
[Link] = amount
[Link] = balance
message = f"Cannot withdraw ${amount}. Balance: ${balance}"
super().__init__(message)

# Use it
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(amount, balance)
return balance - amount

try:
new_balance = withdraw(100, 150)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")

5.6 The get_int() Pattern from CS50


CS50 Python teaches a beautiful pattern: wrapping input() in a loop with exception handling to
create a bulletproof input function. This is used throughout the course.

def get_int(prompt):
"""Keep asking until the user enters a valid integer."""
while True:
try:
return int(input(prompt))
except ValueError:
print("Please enter a whole number.")

age = get_int("Enter your age: ")


print(f"Next year you will be {age + 1}.")

— 50 —
CS50 Python — A Complete Course Companion CS50 Python

# No matter what the user types, this never crashes:


# 'hello' -> 'Please enter a whole number.'
# '3.5' -> 'Please enter a whole number.'
# '25' -> stores 25 and continues

5.7 Common Mistakes

# Mistake 1: Catching too broadly — hiding real bugs


try:
result = complex_calculation()
except: # Hides ALL errors, even bugs!
pass # Silently swallows them

# Fix: catch specific exceptions


except ValueError:
print("Invalid value provided")

# Mistake 2: Putting too much code in try


try:
# 50 lines of code here
...
except ValueError:
pass # Which line caused it? Hard to know.

# Fix: keep try blocks as short as possible — only the risky part

# Mistake 3: Using exceptions for flow control


try:
index = [Link](target) # Don't use exception as if/else
except ValueError:
index = -1

# Better:
index = [Link](target) if target in items else -1

Practice Problems

— 51 —
CS50 Python — A Complete Course Companion CS50 Python

Easy
1. Write a program that asks for two numbers and divides the first by the second. Handle
both ValueError (non-numeric input) and ZeroDivisionError.
2. Write a function safe_open(filename) that opens a file and returns its contents. If the file
doesn't exist, return None.
3. Modify the get_int() pattern to also ensure the integer is positive.

Medium
1. Write a function safe_convert(value, to_type) that attempts to convert a value to the given
type (int, float, str) and returns (True, converted) on success or (False, None) on failure.
2. Create a custom exception AgeError and a function validate_age(age) that raises it for
negative or > 150 values. Write a test program.
3. Write a robust CSV reader function that reads a file line by line. Skip lines that cause
errors and continue processing the rest.

Challenging
1. Build a simple bank account class (no OOP needed — use a dictionary) with deposit,
withdraw, and get_balance functions. Define custom exceptions InsufficientFundsError and
NegativeAmountError.
2. Create a retry decorator (a function that wraps another function) that re-calls the function
up to n times if it raises an exception, then gives up.
3. Write a config file parser that reads key=value pairs from a file. Handle all possible errors:
file not found, malformed lines, duplicate keys. Return a dictionary of valid key-value pairs.

Solutions

# Easy 1
try:
a = float(input("First: "))
b = float(input("Second: "))
print(a / b)
except ValueError:
print("Please enter numbers only.")
except ZeroDivisionError:
print("Cannot divide by zero.")

# Easy 2

— 52 —
CS50 Python — A Complete Course Companion CS50 Python

def safe_open(filename):
try:
with open(filename) as f:
return [Link]()
except FileNotFoundError:
return None

# Medium 1
def safe_convert(value, to_type):
try:
return True, to_type(value)
except (ValueError, TypeError):
return False, None

# Hard 1 — bank account


class InsufficientFundsError(Exception): pass
class NegativeAmountError(Exception): pass

def create_account(initial=0):
return {'balance': initial}

def deposit(account, amount):


if amount <= 0:
raise NegativeAmountError('Amount must be positive')
account['balance'] += amount

def withdraw(account, amount):


if amount <= 0:
raise NegativeAmountError('Amount must be positive')
if amount > account['balance']:
raise InsufficientFundsError('Insufficient funds')
account['balance'] -= amount

Chapter Summary

• An exception is a signal that something went wrong during execution.

• try wraps risky code; except catches specific exceptions that occur.

• else runs only when no exception was raised; finally always runs.

— 53 —
CS50 Python — A Complete Course Companion CS50 Python

• Catch specific exception types — never use a bare except:.

• raise deliberately signals an error from your function.

• Custom exceptions inherit from Exception and make error handling precise.

• Keep try blocks short — only wrap the code that might raise the exception.

• The get_int() loop pattern creates bulletproof user input functions.

— 54 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 6

File Handling

"A program that forgets everything when it ends is a program that never truly
learned. Files give your programs a memory that outlasts any single run."

6.1 Why File Handling Matters

Analo Your brain stores short-term memories for as long as you're awake. But
gy important information — phone numbers, recipes, ideas — gets written
down. A Python program's variables exist only while the program runs. Files
are your program's notebook — persistent storage that survives between
runs.

6.2 Opening and Closing Files

# The basic way (not recommended — you must manually close)


f = open('[Link]', 'r') # 'r' = read mode
content = [Link]()
[Link]() # MUST close to free the resource

# The Pythonic way: with statement (auto-closes the file)


with open('[Link]', 'r') as f:
content = [Link]()
# File is automatically closed here — even if an error occurs

The with statement is a context manager. It guarantees the file is closed when the block exits
— whether by reaching the end normally or by an exception being raised. Always use with
open(...).

File Modes

Mode Meaning File Must Exist?

'r' Read (default) Yes — FileNotFoundError if not

— 55 —
CS50 Python — A Complete Course Companion CS50 Python

'w' Write (overwrites existing content) No — creates file if needed

'a' Append (adds to end) No — creates file if needed

'x' Exclusive create (fails if file exists) No

'r+' Read and write Yes

'rb' Read binary Yes

'wb' Write binary No

6.3 Reading Files

# read() — entire file as one string


with open('[Link]', 'r') as f:
content = [Link]()
print(content)

# readline() — one line at a time


with open('[Link]', 'r') as f:
first_line = [Link]() # Includes the \n at end
second_line = [Link]()

# readlines() — all lines as a list


with open('[Link]', 'r') as f:
lines = [Link]() # ['line1\n', 'line2\n', ...]

# Iterating line by line (most memory-efficient for large files)


with open('[Link]', 'r') as f:
for line in f: # f is iterable!
print([Link]()) # strip() removes trailing \n

6.4 Writing Files

# Write mode: creates or overwrites


with open('[Link]', 'w') as f:
[Link]('Hello, World!\n') # \n adds newline
[Link]('Second line.\n')

# Append mode: adds to end without overwriting

— 56 —
CS50 Python — A Complete Course Companion CS50 Python

with open('[Link]', 'a') as f:


[Link]('New log entry\n')

# Writing multiple lines at once


lines = ['Alice\n', 'Bob\n', 'Charlie\n']
with open('[Link]', 'w') as f:
[Link](lines)

# Using print() to write (adds newline automatically)


with open('[Link]', 'w') as f:
print('Report Title', file=f)
print('Generated by Python', file=f)

6.5 CSV Files


CSV (Comma-Separated Values) is one of the most common data formats in the world.
Python's built-in csv module handles all the edge cases (values with commas inside quotes,
different delimiters, etc.) correctly.

import csv

# Reading a CSV file


with open('[Link]', 'r') as f:
reader = [Link](f)
for row in reader:
print(row) # Each row is a list of strings

# Reading into dictionaries (header row becomes keys)


with open('[Link]', 'r') as f:
reader = [Link](f)
for row in reader:
print(row['name'], row['grade'])

# Writing a CSV file


students = [
{'name': 'Alice', 'grade': 'A'},
{'name': 'Bob', 'grade': 'B'},
]
with open('[Link]', 'w', newline='') as f:
writer = [Link](f, fieldnames=['name', 'grade'])

— 57 —
CS50 Python — A Complete Course Companion CS50 Python

[Link]()
[Link](students)

6.6 JSON Files


JSON (JavaScript Object Notation) is the universal format for structured data exchange.
Python's json module converts between Python objects and JSON text seamlessly.

import json

# Writing Python data to a JSON file


data = {
'name': 'Alice',
'age': 30,
'hobbies': ['reading', 'coding'],
'active': True
}

with open('[Link]', 'w') as f:


[Link](data, f, indent=4) # indent=4 for pretty formatting

# Reading JSON back into Python


with open('[Link]', 'r') as f:
loaded = [Link](f)

print(loaded['name']) # Alice
print(type(loaded)) # <class 'dict'>

# Convert to/from JSON strings (without files)


json_string = [Link](data) # dict -> string
back_to_dict = [Link](json_string) # string -> dict

Python ↔ JSON Type Mapping

Python JSON
------- ----
dict <-> object {}
list <-> array []
str <-> string ""

— 58 —
CS50 Python — A Complete Course Companion CS50 Python

int/float <-> number


True/False <-> true/false
None <-> null

6.7 Common Mistakes

# Mistake 1: Forgetting to close (use 'with' instead)


f = open('[Link]')
# ... code that raises exception ...
[Link]() # Never reached! File stays open.

# Mistake 2: Writing without newlines


with open('[Link]', 'w') as f:
[Link]('line 1') # No \n
[Link]('line 2') # Appears on same line as line 1!

# Mistake 3: Overwriting with 'w' mode when you meant 'a'


with open('[Link]', 'w') as f: # DELETES existing content!
[Link]('new entry')

# Mistake 4: Forgetting newline='' when writing CSV on Windows


with open('[Link]', 'w') as f: # Wrong on Windows
with open('[Link]', 'w', newline='') as f: # Correct

# Mistake 5: Not handling FileNotFoundError


with open('might_not_exist.txt') as f: # Crash if missing
...
# Fix:
try:
with open('might_not_exist.txt') as f:
...
except FileNotFoundError:
print('File not found.')

Practice Problems

Easy

— 59 —
CS50 Python — A Complete Course Companion CS50 Python

1. Write a program that asks the user to enter 5 names and saves them to '[Link]', one
per line.
2. Write a program that reads '[Link]' and prints each name in uppercase.
3. Write a program that appends the current date and time to '[Link]' each time it runs.

Medium
1. Create a contacts CSV file with columns name, phone, email. Write a program that reads
it and lets the user search by name.
2. Write a program that reads a text file and counts: total words, unique words, and the 5
most common words.
3. Write a simple student grade book: read from JSON on startup, allow adding students
and grades, save back to JSON on exit.

Challenging
1. Build a CSV to JSON converter: read any CSV file and convert it to a JSON array of
objects, where each row becomes an object with column headers as keys.
2. Write a log file analyzer: read a server log where each line is 'timestamp | level |
message'. Count entries by level (INFO, WARNING, ERROR) and show the 5 most recent
errors.
3. Implement a simple file-based database with functions: insert(table, record), find(table,
key, value), and delete(table, key, value). Store everything as JSON.

Solutions

# Easy 1
with open('[Link]', 'w') as f:
for _ in range(5):
name = input('Enter name: ')
[Link](name + '\n')

# Easy 2
with open('[Link]', 'r') as f:
for line in f:
print([Link]().upper())

# Medium 2 — word counter


from collections import Counter

— 60 —
CS50 Python — A Complete Course Companion CS50 Python

with open('[Link]', 'r') as f:


text = [Link]().lower()
words = [Link]()
counter = Counter(words)
print(f'Total words: {len(words)}')
print(f'Unique words: {len(counter)}')
print('Top 5:', counter.most_common(5))

# Hard 1 — CSV to JSON


import csv, json
def csv_to_json(csv_file, json_file):
with open(csv_file, 'r') as f:
reader = [Link](f)
rows = list(reader)
with open(json_file, 'w') as f:
[Link](rows, f, indent=2)
print(f'Converted {len(rows)} rows.')

Chapter Summary

• Always use 'with open(...)' — it guarantees the file is closed even if an error occurs.

• File modes: 'r' (read), 'w' (write/overwrite), 'a' (append), 'x' (exclusive create).

• read() gets the whole file; readline() gets one line; iterating the file object is most
efficient.

• write() does not add newlines — add \n explicitly or use print(..., file=f).

• The csv module handles all CSV edge cases — never split on commas manually.

• DictReader and DictWriter make CSV feel like working with dictionaries.

• [Link]() writes Python objects to JSON files; [Link]() reads them back.

• Always handle FileNotFoundError when opening files that might not exist.

— 61 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 7

Libraries and Modules

"Don't reinvent the wheel. Stand on the shoulders of giants. Every time you
import a library, you inherit decades of work."

7.1 Why Libraries Exist

Analo When you build a house, you don't mine the ore and smelt the steel for your
gy nails. You go to a hardware store. A library is Python's hardware store:
thousands of pre-built components for sorting, math, networking, web
scraping, machine learning, and everything in between.

7.2 Importing Modules

# Import an entire module


import math
print([Link](16)) # 4.0
print([Link]) # 3.141592653589793
print([Link](3.7)) # 3
print([Link](3.2)) # 4

# Import specific names from a module


from math import sqrt, pi
print(sqrt(25)) # 5.0 (no 'math.' prefix needed)
print(pi) # 3.141592...

# Import with an alias


import random as rnd
print([Link](1, 10)) # Random integer 1-10

# Import everything (use sparingly — can cause name conflicts)


from math import *

— 62 —
CS50 Python — A Complete Course Companion CS50 Python

Which Style to Use?


• import module — safest; makes it clear where everything comes from.
• from module import name — good for things you use frequently.
• import module as alias — standard for well-known libraries (import numpy as np).
• from module import * — avoid in production code; pollutes namespace.

7.3 Key Standard Library Modules

random — Generating Random Values

import random

print([Link](1, 6)) # Random int 1-6 (dice roll)


print([Link]()) # Random float 0.0 to 1.0
print([Link](['a','b','c'])) # Random item from list

numbers = [1, 2, 3, 4, 5]
[Link](numbers) # Shuffles list in place
print(numbers)

print([Link](range(1,50), 6)) # 6 unique lottery numbers

sys — System Information and Control

import sys

print([Link]) # Python version string


print([Link]) # Command-line arguments as a list
# [Link][0] is the script name
[Link]() # Exit the program immediately
[Link](1) # Exit with error code 1

os — Operating System Interface

import os

print([Link]()) # Current working directory


[Link]('new_folder') # Create a directory

— 63 —
CS50 Python — A Complete Course Companion CS50 Python

[Link]('[Link]','[Link]') # Rename a file


[Link]('[Link]') # Delete a file
print([Link]('.')) # List files in current directory
print([Link]('[Link]')) # True if file exists
print([Link]('dir', '[Link]')) # Safe path joining

datetime — Dates and Times

from datetime import datetime, date, timedelta

now = [Link]()
today = [Link]()

print([Link], [Link], [Link])


print([Link]('%Y-%m-%d %H:%M:%S')) # Format as string

# Date arithmetic
tomorrow = today + timedelta(days=1)
last_week = today - timedelta(weeks=1)

# Parse a date string


birthday = [Link]('1990-05-15', '%Y-%m-%d')
print([Link]) # 1990

collections — Specialized Data Structures

from collections import Counter, defaultdict, namedtuple

# Counter: count occurrences


words = ['apple', 'banana', 'apple', 'cherry', 'apple']
counter = Counter(words)
print(counter) # Counter({'apple': 3, 'banana': 1, ...})
print(counter.most_common(2)) # [('apple', 3), ('banana', 1)]

# defaultdict: dict with default value type


groups = defaultdict(list) # Default value is an empty list
groups['A'].append('Alice') # No KeyError even for new keys
groups['B'].append('Bob')

# namedtuple: tuple with named fields

— 64 —
CS50 Python — A Complete Course Companion CS50 Python

Point = namedtuple('Point', ['x', 'y'])


p = Point(3, 4)
print(p.x, p.y) # 3 4

7.4 Installing Third-Party Libraries


Python's package manager is pip. It downloads and installs libraries from PyPI (Python
Package Index), which hosts over 400,000 open-source packages.

# In your terminal (not in Python):


pip install requests # Install the 'requests' library
pip install numpy pandas # Install multiple at once
pip list # Show all installed packages
pip uninstall requests # Remove a package

# After installing, use it in Python:


import requests
response = [Link]('[Link]
print(response.status_code) # 200 means success
print([Link]()) # Parse JSON response

7.5 Creating Your Own Modules


Any Python file is a module. If you save functions in a file called [Link], you can import
them into any other file in the same directory.

# File: [Link]
def square(n):
return n * n

def cube(n):
return n * n * n

PI = 3.14159

# File: [Link] (in the same directory)


from helpers import square, cube, PI

print(square(4)) # 16

— 65 —
CS50 Python — A Complete Course Companion CS50 Python

print(cube(3)) # 27
print(PI) # 3.14159

The __name__ == '__main__' Pattern


When Python runs a file directly, it sets the special variable __name__ to '__main__'. When
the file is imported as a module, __name__ is set to the module's filename. This pattern lets
you write code that only runs when the file is the entry point:

# File: [Link]
def square(n):
return n * n

# This block only runs when [Link] is run directly,


# NOT when it is imported by another file:
if __name__ == '__main__':
print('Testing square function:')
print(square(5)) # 25

Practice Problems

Easy
1. Use the random module to simulate rolling two six-sided dice 10 times. Print each result
and count how many times you rolled doubles.
2. Use the datetime module to print today's date in the format 'Day, Month DD YYYY' (e.g.,
'Monday, January 15 2024').
3. Use the os module to list all .py files in the current directory.

Medium
1. Write a word frequency counter using [Link]. Read from a text file and
display the top 10 most common words, excluding common stop words (the, a, is, etc.).
2. Create a module called [Link] with functions for area and perimeter of circle,
rectangle, and triangle. Import and test it in a [Link] file.
3. Build a simple dice statistics program: roll a die n times (user chooses n). Use Counter to
display how many times each face appeared and the percentage.

— 66 —
CS50 Python — A Complete Course Companion CS50 Python

Challenging
1. Use the requests library to fetch data from a public API (e.g.,
[Link] for Bitcoin price). Parse the JSON
response and display a formatted summary.
2. Create a command-line tool using [Link]. The user runs it as 'python [Link] add 5 3',
'python [Link] multiply 4 6', etc. Parse the arguments and perform the requested operation.
3. Build a file organizer: scan a directory, group files by extension, create subdirectories for
each group, and move files into them.

Chapter Summary

• import module loads a module; from module import name imports specific names.

• Use import module as alias for long or commonly aliased names (numpy as np).

• The Python standard library has modules for almost everything: math, random, os,
sys, datetime, json, csv.

• pip install package installs third-party libraries from PyPI.

• Any .py file is a module — save functions there and import them anywhere.

• if __name__ == '__main__': prevents test code from running on import.

• [Link] makes counting frequencies trivially easy.

• datetime handles all date/time arithmetic and formatting.

— 67 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 8

Regular Expressions

"Some people, when confronted with a problem, think: 'I know, I'll use regular
expressions.' Now they have two problems. — Jamie Zawinski. (The point:
learn them well and they become one very powerful solution.)"

8.1 What Are Regular Expressions?

Analo Imagine you're searching for all phone numbers in a 1000-page document.
gy You can't search for a specific number — you need to search for the
PATTERN: three digits, a dash, three digits, a dash, four digits. A regular
expression is a precise description of a pattern in text.

A regular expression (regex) is a sequence of characters that defines a search pattern. They
are used for: validating input (is this a valid email?), searching text (find all dates), extracting
data (pull all URLs from HTML), and transforming text (replace all phone formats with a
standard format).

8.2 The re Module

import re

# [Link](pattern, string) — find first match anywhere in string


result = [Link](r'cat', 'The cat sat on the mat')
if result:
print('Found:', [Link]()) # Found: cat
print('Position:', [Link](), '-', [Link]())

# [Link](pattern, string) — match only at START of string


result = [Link](r'The', 'The cat sat')
print(bool(result)) # True

result = [Link](r'cat', 'The cat sat')


print(bool(result)) # False — 'cat' is not at the start

— 68 —
CS50 Python — A Complete Course Companion CS50 Python

# [Link](pattern, string) — return all non-overlapping matches


matches = [Link](r'at', 'The cat sat on the mat')
print(matches) # ['at', 'at', 'at']

# [Link](pattern, replacement, string) — substitute matches


result = [Link](r'cat', 'dog', 'The cat sat on the mat')
print(result) # 'The dog sat on the mat'

Notice the r prefix before pattern strings: r'pattern'. This is a raw string — backslashes
are treated literally, not as escape sequences. Always use raw strings for regular expressions
to avoid unexpected behavior.

8.3 Pattern Building Blocks

Literal Characters and Dots

# Literal characters match themselves


[Link](r'python', 'I love python!') # Matches 'python'

# . (dot) matches ANY single character except newline


[Link](r'c.t', 'cat cut cot c3t c t') # ['cat','cut','cot','c3t']

Character Classes [ ]

# [abc] matches a, b, or c
[Link](r'[aeiou]', 'hello world') # ['e', 'o', 'o']

# [a-z] matches any lowercase letter


# [A-Z] matches any uppercase letter
# [0-9] matches any digit
# [a-zA-Z0-9] matches any letter or digit

# [^abc] matches anything EXCEPT a, b, or c


[Link](r'[^aeiou]', 'hello') # ['h', 'l', 'l']

Special Sequences

Sequence Matches Equivalent

— 69 —
CS50 Python — A Complete Course Companion CS50 Python

\d Any digit [0-9]

\D Any non-digit [^0-9]

\w Word character [a-zA-Z0-9_]

\W Non-word character [^a-zA-Z0-9_]

\s Whitespace [ \t\n\r]

\S Non-whitespace [^ \t\n\r]

\b Word boundary Between word and non-word

Quantifiers — How Many Times?


Quantifier Meaning Example

* 0 or more a* matches '', 'a', 'aa', 'aaa'...

+ 1 or more a+ matches 'a', 'aa', 'aaa'...

? 0 or 1 (optional) colou?r matches 'color' and 'colour'

{n} Exactly n times \d{3} matches exactly 3 digits

{n,} n or more times \d{2,} matches 2 or more digits

{n,m} Between n and m times \d{2,4} matches 2, 3, or 4 digits

Anchors — Position in the String

# ^ anchors to the START of string


[Link](r'^Hello', 'Hello World') # Match
[Link](r'^World', 'Hello World') # No match

# $ anchors to the END of string


[Link](r'World$', 'Hello World') # Match
[Link](r'Hello$', 'Hello World') # No match

# Validate that ENTIRE string is digits only


[Link](r'\d+', '12345') # Match
[Link](r'\d+', '123abc') # No match

8.4 Groups — Capturing Parts of a Match

— 70 —
CS50 Python — A Complete Course Companion CS50 Python

# Parentheses create groups


pattern = r'(\d{4})-(\d{2})-(\d{2})' # Date: YYYY-MM-DD
text = 'Today is 2024-01-15, meeting on 2024-02-20'

# Find first match and extract groups


match = [Link](pattern, text)
if match:
print([Link](0)) # '2024-01-15' — entire match
print([Link](1)) # '2024' — first group
print([Link](2)) # '01' — second group
print([Link](3)) # '15' — third group

# Find ALL matches


dates = [Link](pattern, text)
print(dates) # [('2024', '01', '15'), ('2024', '02', '20')]

8.5 Practical Examples

import re

# Email validation
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool([Link](pattern, email))

print(is_valid_email('alice@[Link]')) # True
print(is_valid_email('not_an_email')) # False

# Phone number extraction


text = 'Call 555-123-4567 or +1 (800) 555-0199 for info.'
phones = [Link](r'[\+\(]?[\d\s\-\(\)]{7,15}', text)
print(phones)

# URL extraction
html = '<a href="[Link]
urls = [Link](r'https?://[^"]+', html)
print(urls) # ['[Link]

# Replace multiple whitespace with single space

— 71 —
CS50 Python — A Complete Course Companion CS50 Python

messy = 'Hello World how are you'


clean = [Link](r'\s+', ' ', messy)
print(clean) # 'Hello World how are you'

Practice Problems

Easy
1. Write a function that checks if a string contains only alphabetic characters (no digits or
spaces).
2. Write a function that extracts all numbers (integers and decimals) from a string.
3. Write a function that checks if a string is a valid US zip code (5 digits, or 5+4 format:
12345-6789).

Medium
1. Write a function that validates a password: at least 8 characters, at least one uppercase
letter, at least one digit, and at least one special character (!@#$%^&*).
2. Write a function that finds all HTML tags in a string and returns a list of tag names without
the angle brackets. Example: 'Hello World' -> ['p', 'b', '/b'].
3. Write a program that reads a text file and finds all email addresses in it.

Challenging
1. Write a function that parses a log line of the format '[2024-01-15 14:23:01] ERROR
[Link] -- Connection refused' and returns a dictionary with keys: date, time, level, file,
line, message.
2. Write a function that takes a Markdown string and converts it to plain text by removing all
Markdown formatting (headers #, bold **, italic *, links [text](url)).
3. Build a simple template engine: given a template string with {{variable}} placeholders and
a dictionary of values, replace all placeholders with their values using [Link].

Chapter Summary

• Regular expressions describe text patterns — they're a mini-language inside


Python.

• Always use raw strings (r'pattern') for regex patterns.

— 72 —
CS50 Python — A Complete Course Companion CS50 Python

• [Link]() finds first match anywhere; [Link]() only at start; [Link]() returns all.

• [Link]() replaces matches; [Link]() validates entire string.

• Character classes [abc], special sequences (\d \w \s), and quantifiers (* + ? {n,m})
are the core vocabulary.

• ^ anchors to start, $ to end; \b matches word boundaries.

• Parentheses () create groups that can be extracted with [Link](n).

• Test your patterns interactively at [Link] before using them in code.

— 73 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER 9

Object-Oriented Programming

"Object-oriented programming is a way of thinking about problems. Instead of


thinking 'what should happen next', you think 'what things exist, and what can
they do?'"

9.1 Why OOP Exists

Analo Imagine designing a video game with 100 different types of characters —
gy warriors, mages, archers. Each character has health, a name, attack power.
Without OOP, you'd have 300 separate variables just for health values. With
OOP, you define a Character blueprint once. Then you stamp out as many
characters as you need — each one is its own independent object with its
own data.

As programs grow larger, the procedural approach (functions and variables at the top level)
becomes hard to manage. OOP organizes code into objects — self-contained units that
combine data (attributes) and behavior (methods). This makes large programs comprehensible,
maintainable, and extensible.

9.2 Classes and Objects


A class is a blueprint. An object (or instance) is a specific thing created from that blueprint.
The class defines what attributes and methods all objects of that type will have. Each object
then has its own personal copy of those attributes.

CLASS: Dog (blueprint)


■■■■■■■■■■■■■■■■■■■■■■■■
■ Attributes: ■
■ name, breed, age ■
■ Methods: ■
■ bark(), eat() ■
■■■■■■■■■■■■■■■■■■■■■■■■
|
■■■■■■■■■■■■■■■

— 74 —
CS50 Python — A Complete Course Companion CS50 Python

| |
OBJECT OBJECT
name='Buddy' name='Max'
breed='Lab' breed='Poodle'
age=3 age=5
(each has its own data, same blueprint)

9.3 Defining a Class

class Dog:
"""Represents a dog."""

# Class attribute — shared by ALL Dog objects


species = 'Canis lupus familiaris'

def __init__(self, name, breed, age):


"""Initialize a new Dog object."""
# Instance attributes — unique to each object
[Link] = name
[Link] = breed
[Link] = age

def bark(self):
"""Make the dog bark."""
print(f"{[Link]} says: Woof!")

def describe(self):
"""Print a description of the dog."""
print(f"{[Link]} is a {[Link]}-year-old {[Link]}.")

# Creating objects (instances)


buddy = Dog('Buddy', 'Labrador', 3)
max = Dog('Max', 'Poodle', 5)

[Link]() # Buddy says: Woof!


[Link]() # Max is a 5-year-old Poodle.
print([Link]) # Canis lupus familiaris

— 75 —
CS50 Python — A Complete Course Companion CS50 Python

9.4 The __init__ Method


The __init__ method is the constructor — it runs automatically every time you create a new
object. It initializes the object's instance attributes with the values passed in.

buddy = Dog('Buddy', 'Labrador', 3)


|
Python calls: Dog.__init__(buddy, 'Buddy', 'Labrador', 3)
| | | | |
| self name breed age
|
Inside __init__:
[Link] = 'Buddy' <- [Link] = 'Buddy'
[Link] = 'Labrador' <- [Link] = 'Labrador'
[Link] = 3 <- [Link] = 3

The self parameter is a reference to the object being created. Python passes it automatically
— you never pass it manually when calling methods. It is how an object refers to its own data.

9.5 Methods
Methods are functions defined inside a class. They always receive self as their first parameter
— this is how they access the object's attributes.

class BankAccount:
def __init__(self, owner, balance=0):
[Link] = owner
[Link] = balance
[Link] = []

def deposit(self, amount):


if amount <= 0:
raise ValueError('Deposit must be positive')
[Link] += amount
[Link](f'Deposit: +${amount}')

def withdraw(self, amount):


if amount > [Link]:
raise ValueError('Insufficient funds')
[Link] -= amount
[Link](f'Withdrawal: -${amount}')

— 76 —
CS50 Python — A Complete Course Companion CS50 Python

def get_balance(self):
return [Link]

def print_statement(self):
print(f'Account: {[Link]}')
for entry in [Link]:
print(f' {entry}')
print(f' Balance: ${[Link]}')

# Using the class


acc = BankAccount('Alice', 1000)
[Link](500)
[Link](200)
acc.print_statement()

9.6 Inheritance — Building on Existing Classes

Analo Every mammal breathes air and nurses its young. A dog is a mammal, so it
gy gets those capabilities for free. But a dog also barks, which other mammals
don't. Inheritance lets a child class inherit all the attributes and methods of a
parent class and then add or override what it needs.

class Animal:
def __init__(self, name, sound):
[Link] = name
[Link] = sound

def speak(self):
print(f'{[Link]} says {[Link]}!')

def __str__(self):
return f'Animal: {[Link]}'

# Dog inherits from Animal


class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name, 'Woof') # Call parent __init__
[Link] = breed

— 77 —
CS50 Python — A Complete Course Companion CS50 Python

def fetch(self, item):


print(f'{[Link]} fetches the {item}!')

class Cat(Animal):
def __init__(self, name):
super().__init__(name, 'Meow')

def purr(self):
print(f'{[Link]} purrs...')

dog = Dog('Buddy', 'Labrador')


cat = Cat('Whiskers')

[Link]() # Buddy says Woof! (inherited)


[Link]('ball') # Buddy fetches the ball! (own method)
[Link]() # Whiskers says Meow!
[Link]() # Whiskers purrs...

Method Overriding

class Shape:
def area(self):
return 0 # Default implementation

class Circle(Shape):
def __init__(self, radius):
[Link] = radius

def area(self): # Override parent's area()


return 3.14159 * [Link] ** 2

class Rectangle(Shape):
def __init__(self, width, height):
[Link] = width
[Link] = height

def area(self): # Override parent's area()


return [Link] * [Link]

— 78 —
CS50 Python — A Complete Course Companion CS50 Python

shapes = [Circle(5), Rectangle(4, 6), Circle(3)]


for shape in shapes:
print(f'Area: {[Link]():.2f}') # Polymorphism!

The last example demonstrates polymorphism — the ability to call the same method
(area()) on different object types and get the correct behavior for each. Python figures out
which version to call based on the object's actual type at runtime.

9.7 Special Methods — Making Objects Feel Pythonic


Special methods (also called dunder methods — double underscore) let your objects behave
like built-in Python types. They are called automatically by Python in specific situations.

class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
# Called by print() and str()
return f'Vector({self.x}, {self.y})'

def __repr__(self):
# Called in interactive shell and by repr()
return f'Vector(x={self.x}, y={self.y})'

def __add__(self, other):


# Called by the + operator
return Vector(self.x + other.x, self.y + other.y)

def __eq__(self, other):


# Called by == operator
return self.x == other.x and self.y == other.y

def __len__(self):
# Called by len()
import math
return int([Link](self.x**2 + self.y**2))

v1 = Vector(2, 3)
v2 = Vector(1, 4)

— 79 —
CS50 Python — A Complete Course Companion CS50 Python

print(v1) # Vector(2, 3) — uses __str__


v3 = v1 + v2 # Uses __add__ -> Vector(3, 7)
print(v3) # Vector(3, 7)
print(v1 == v2) # False — uses __eq__

9.8 Encapsulation — Protecting Data


Encapsulation means keeping an object's internal data hidden from the outside world. You
interact with an object only through its defined methods — not by reaching in and changing its
attributes directly. This prevents unintended side effects.

class Temperature:
def __init__(self, celsius):
self._celsius = celsius # _ prefix = 'private by convention'

@property
def celsius(self):
return self._celsius

@[Link]
def celsius(self, value):
if value < -273.15:
raise ValueError('Temperature below absolute zero!')
self._celsius = value

@property
def fahrenheit(self):
return self._celsius * 9/5 + 32

t = Temperature(25)
print([Link]) # 25 — uses getter
print([Link]) # 77.0 — computed property
[Link] = 100 # Uses setter (with validation)
[Link] = -300 # ValueError!

9.9 Common Mistakes

# Mistake 1: Forgetting 'self' in method definition


class Dog:

— 80 —
CS50 Python — A Complete Course Companion CS50 Python

def bark(): # Missing self!


print('Woof')

d = Dog()
[Link]() # TypeError: bark() takes 0 args, 1 given

class Dog:
def bark(self): # Correct
print('Woof')

# Mistake 2: Forgetting to call super().__init__()


class Dog(Animal):
def __init__(self, name, breed):
# If you don't call super().__init__(),
# Animal's attributes (name, sound) are not set up!
[Link] = breed

# Mistake 3: Using class attributes for mutable defaults


class Student:
grades = [] # WRONG — shared by ALL students!

def __init__(self, name):


[Link] = name
[Link] = [] # CORRECT — each student gets own list

# Mistake 4: Modifying an object's private attributes directly


t = Temperature(25)
t._celsius = -500 # Works but bypasses validation — bad practice

Practice Problems

Easy
1. Create a Rectangle class with width and height attributes and methods area() and
perimeter(). Test with several instances.
2. Create a Student class with name and grades (a list). Add methods add_grade(g),
average(), and highest_grade().
3. Create a Counter class that starts at 0 and has methods increment(), decrement(),
reset(), and value().

— 81 —
CS50 Python — A Complete Course Companion CS50 Python

Medium
1. Create a Book class and a Library class. Library has a list of Books. Add methods
add_book(), remove_book(), find_by_author(), and list_all().
2. Create a Shape hierarchy: base class Shape with an abstract area() method, then Circle,
Rectangle, and Triangle as subclasses. Each implements area(). Write a function
total_area(shapes) that sums areas of any mix of shapes.
3. Create a Stack class (LIFO data structure) using a list internally. Add push(), pop(),
peek() (view top without removing), is_empty(), and size().

Challenging
1. Implement a simple linked list with a Node class and a LinkedList class. LinkedList should
support append(), prepend(), delete(value), find(value), and __str__() for printing.
2. Build a simple ORM (Object-Relational Mapper) backed by a dictionary: a Model base
class with save(), delete(), find_by_id() class methods, and a way to define fields. Create
User and Product as subclasses.
3. Design a card game: create Card, Deck, Hand, and Player classes. Deck should shuffle
and deal cards. Implement a simple game like War where two players draw cards and the
higher card wins.

Solutions

# Easy 1 — Rectangle
class Rectangle:
def __init__(self, width, height):
[Link] = width
[Link] = height

def area(self): return [Link] * [Link]


def perimeter(self): return 2 * ([Link] + [Link])
def __str__(self): return f'Rectangle({[Link]}x{[Link]})'

# Easy 3 — Counter
class Counter:
def __init__(self): self._count = 0
def increment(self): self._count += 1
def decrement(self): self._count -= 1
def reset(self): self._count = 0
def value(self): return self._count

— 82 —
CS50 Python — A Complete Course Companion CS50 Python

# Medium 3 — Stack
class Stack:
def __init__(self): self._items = []
def push(self, item): self._items.append(item)
def pop(self):
if self.is_empty(): raise IndexError('Stack is empty')
return self._items.pop()
def peek(self):
if self.is_empty(): raise IndexError('Stack is empty')
return self._items[-1]
def is_empty(self): return len(self._items) == 0
def size(self): return len(self._items)

Chapter Summary

• A class is a blueprint; an object (instance) is a specific creation from that blueprint.

• __init__ is the constructor — it runs automatically when an object is created.

• self refers to the object itself; all instance methods must accept it as first parameter.

• Instance attributes belong to one object; class attributes are shared by all instances.

• Inheritance lets a child class reuse and extend a parent class.

• super() calls the parent class's method — essential in __init__ of child classes.

• Polymorphism lets different object types respond correctly to the same method call.

• Special methods (__str__, __add__, __eq__, etc.) make objects integrate with
Python syntax.

• Encapsulation uses _ prefix and @property to protect and validate attribute access.

• Never use mutable objects (lists, dicts) as class-level default attributes.

— 83 —
CS50 Python — A Complete Course Companion CS50 Python

CHAPTER

Closing Notes — What Comes Next

"Programming is not a destination. It is a practice. The best programmers are


not those who know the most — they are those who are most comfortable not
knowing, and most skilled at finding out."

You Have Completed CS50 Python


If you have worked through all nine chapters of this book — reading, coding, debugging, and
solving problems — you are no longer a beginner. You understand how Python represents
data, makes decisions, repeats operations, organizes code into functions, handles errors
gracefully, works with files, uses libraries, matches patterns in text, and models real-world
concepts as objects. That is the complete foundation.

Where to Go From Here


1. Data Science — Learn NumPy, Pandas, and Matplotlib. Work with real datasets.
[Link] has thousands of free datasets and notebooks.
2. Web Development — Learn Flask or Django to build web applications. These
frameworks are built on every concept in this book.
3. Automation — Use Python to automate repetitive tasks: web scraping with
BeautifulSoup or Selenium, sending emails with smtplib, interacting with APIs.
4. Machine Learning — Once you're comfortable with NumPy and Pandas, explore
scikit-learn for classical ML and PyTorch or TensorFlow for deep learning.
5. CS50 Itself — If you haven't taken CS50 at [Link], do it. It is free, rigorous,
and genuinely life-changing.
6. Build Something — This is the most important one. Pick a problem you actually care
about and build a program to solve it. There is no better teacher than building.

The Final Word


Every expert programmer you admire once stared at an error message they couldn't
understand. They Googled things you think they would know by heart. They wrote code they
later deleted in embarrassment. The gap between beginner and expert is not intelligence. It is
time, curiosity, and the stubborn refusal to stay stuck.

— 84 —
CS50 Python — A Complete Course Companion CS50 Python

You have the foundation. Now go build something. The Python community, Stack Overflow, the
documentation at [Link], and this book will always be here when you need them.

Good luck. You've earned it.

— 85 —

You might also like