The Little Book of Python
The Little Book of Python
Version 0.1.0
Duc-Tam Nguyen
2025-09-15
Table of contents
2
Chapter 4. Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
31. Defining a Function (def) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
32. Function Arguments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
33. Default & Keyword Arguments . . . . . . . . . . . . . . . . . . . . . . . . . 67
34. Return Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
35. Variable Scope (local vs global) . . . . . . . . . . . . . . . . . . . . . . . . . 72
36. *args and kwargs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
37. Lambda Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
38. Docstrings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
39. Recursive Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
40. Higher-Order Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Chapter 5. Modules and Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
41. Importing Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
42. Built-in Modules (math, random) . . . . . . . . . . . . . . . . . . . . . . . . 87
43. Aliasing Imports (import ... as ...) . . . . . . . . . . . . . . . . . . . . 89
44. Importing Specific Functions . . . . . . . . . . . . . . . . . . . . . . . . . . 91
45. dir() and help() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
46. Creating Your Own Module . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
47. Understanding Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
48. Using pip to Install Packages . . . . . . . . . . . . . . . . . . . . . . . . . . 99
49. Virtual Environments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
50. Popular Third-Party Packages (Overview) . . . . . . . . . . . . . . . . . . . 103
Chapter 6. File Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
51. Opening Files (open) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
52. Reading Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
53. Writing Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
54. File Modes (r, w, a, b) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
55. Closing Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
56. Using with Context Manager . . . . . . . . . . . . . . . . . . . . . . . . . . 115
57. Working with CSV Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
58. Working with JSON Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
59. File Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
60. Paths & Directories (os, pathlib) . . . . . . . . . . . . . . . . . . . . . . . 123
Chapter 7. Object-Oriented Python . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
61. Classes & Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
62. Attributes & Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
63. __init__ Constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
64. Instance vs Class Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
65. Inheritance Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
66. Method Overriding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
67. Multiple Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
68. Encapsulation & Private Members . . . . . . . . . . . . . . . . . . . . . . . 143
69. Special Methods (__str__, __len__, etc.) . . . . . . . . . . . . . . . . . . . 145
3
70. Static & Class Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Chapter 8. Error Handling and Exceptions . . . . . . . . . . . . . . . . . . . . . . . 150
71. What Are Exceptions? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
72. Common Exceptions (ValueError, TypeError, etc.) . . . . . . . . . . . . . 152
73. try and except Blocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
74. Catching Multiple Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . 155
75. else in Exception Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
76. finally Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
77. Raising Exceptions (raise) . . . . . . . . . . . . . . . . . . . . . . . . . . . 161
78. Creating Custom Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . 163
79. Assertions (assert) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
80. Best Practices for Error Handling . . . . . . . . . . . . . . . . . . . . . . . 167
Chapter 9. Advanced Python Features . . . . . . . . . . . . . . . . . . . . . . . . . . 170
81. List Comprehensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
82. Dictionary Comprehensions . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
83. Set Comprehensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
84. Generators (yield) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
85. Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
86. Decorators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
87. Context Managers (Custom) . . . . . . . . . . . . . . . . . . . . . . . . . . 183
88. with and Resource Management . . . . . . . . . . . . . . . . . . . . . . . . 185
89. Modules itertools & functools . . . . . . . . . . . . . . . . . . . . . . . 187
90. Type Hints (typing Module) . . . . . . . . . . . . . . . . . . . . . . . . . . 190
Chapter 10. Python in Practices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
91. REPL & Interactive Mode . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
92. Debugging (pdb) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
93. Logging (logging Module) . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
94. Unit Testing (unittest) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
95. Virtual Environments Best Practice . . . . . . . . . . . . . . . . . . . . . . 202
96. Writing a Simple Script . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 204
97. CLI Arguments (argparse) . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
98. Working with APIs (requests) . . . . . . . . . . . . . . . . . . . . . . . . . 209
99. Basics of Web Scraping (BeautifulSoup) . . . . . . . . . . . . . . . . . . . 211
100. Next Steps: Where to Go from Here . . . . . . . . . . . . . . . . . . . . . 213
4
The Little Book of Python
1. What is Python?
Deep Dive
• Versatility: Python is sometimes called a “glue language” because it can integrate with
other systems and languages easily. You can call C or C++ libraries, run shell commands,
or embed Python into other applications.
5
• Community and ecosystem: With millions of developers worldwide, Python has a massive
community. This means a wealth of tutorials, open-source projects, and support forums
are available for learners and professionals.
• Libraries and frameworks: Python has specialized libraries for nearly every domain:
Python’s balance of simplicity and power makes it an excellent first language for beginners, yet
powerful enough for advanced engineers building production-grade systems.
Tiny Code
Why it Matters
Python matters because it lowers the barrier to entry into programming. Its readability
and straightforward syntax make it an ideal starting point for newcomers, while its depth
and ecosystem allow professionals to tackle complex problems in machine learning, finance,
cybersecurity, and more. Learning Python often serves as a gateway to the broader world of
computer science and software engineering.
6
Try It Yourself
3. Run import this in the Python shell and read through the Zen of Python. Which line
resonates with you most, and why?
This exercise introduces you to Python’s core design philosophy while letting you experience
the simplicity of writing and running your first code.
Python is available for almost every operating system, and installing it is the first step before
you can write and execute your own programs. Most modern computers already come with
Python preinstalled, but often it is not the latest version. For development, it is generally
recommended to use the most recent stable release (for example, Python 3.12).
Deep Dive
• On Windows, download the installer from the official website python.org. During instal-
lation, make sure to check the box “Add Python to PATH” so you can run Python from
the command line.
• On macOS, you can use Homebrew (brew install python) or download from
python.org.
• On Linux, Python is usually preinstalled. If not, use your package manager (sudo apt
install python3 on Ubuntu/Debian, sudo dnf install python3 on Fedora).
After installation, open your terminal (or command prompt) and type:
python3 --version
This should display something like Python 3.14.0. If it doesn’t, the installation or PATH
configuration may need adjustment.
Running the Interpreter (REPL):
You can enter interactive mode by typing python or python3 in your terminal. This launches
the Read-Eval-Print Loop (REPL), where you can execute code line by line:
7
>>> 2 + 3
5
>>> print("Hello, Python!")
Hello, Python!
Running Scripts:
While the REPL is good for quick experiments, most real programs are saved in files with a
.py extension. You can create a file hello.py containing:
python3 hello.py
Tiny Code
# File: hello.py
name = "Ada"
print("Hello,", name)
To run:
8
python3 hello.py
Why it Matters
Understanding how to install Python and run scripts is fundamental because it gives you
control over your development environment. Without mastering this, you can’t progress to
building real applications. Installing properly also ensures you have access to the latest features
and security updates.
Try It Yourself
This exercise ensures you can not only experiment interactively but also save and execute
complete programs.
Python’s syntax is designed to be simple and human-readable. Unlike many other programming
languages that use braces {} or keywords to define code blocks, Python uses indentation (spaces
or tabs). This is not optional—correct indentation is part of Python’s grammar. The focus on
clean and consistent code is one of the reasons why Python is popular both in education and
professional development.
Deep Dive
• Indentation Instead of Braces: In languages like C, C++, or Java, you often see:
if (x > 0) {
printf("Positive\n");
}
9
The colon (:) signals the start of a new block, and the indented lines that follow belong
to that block.
• Consistency Matters: Python requires consistency in indentation. You cannot mix
tabs and spaces within the same block. The most common convention is 4 spaces per
indentation level.
• Nested Indentation: Blocks can be nested by increasing indentation further:
if x > 0:
if x % 2 == 0:
print("Positive and even")
else:
print("Positive and odd")
• Line Continuation: Long lines can be split with \ or by wrapping expressions inside
parentheses:
total = (100 + 200 + 300 +
400 + 500)
• Comments: Python uses # for single-line comments and triple quotes (""" ... """) for
docstrings or multi-line comments.
Tiny Code
10
Why it Matters
Indentation rules enforce consistency across all Python code. This reduces errors caused
by messy formatting and makes programs easier to read, especially when working in teams.
Python’s syntax philosophy ensures beginners learn clean habits from the start and professionals
maintain readability in large projects.
Try It Yourself
1. Write a program that checks if a number is positive, negative, or zero using proper
indentation.
2. Experiment by removing indentation or mixing spaces and tabs—notice how Python
raises an IndentationError.
3. Write nested if statements to check whether a number is divisible by both 2 and 3.
This will help you experience firsthand why Python enforces indentation and how it guides you
to write clean, structured code.
In Python, a variable is like a box with a name where you can store information. You can put
numbers, text, or other kinds of data inside that box, and later use the name of the box to get
the value back.
Unlike some languages, you don’t need to say what kind of data will go inside the box—Python
figures it out for you automatically.
Deep Dive
• Creating a Variable: You just choose a name and use the equals sign = to assign a value:
age = 20
name = "Alice"
height = 1.75
• Naming Rules:
11
– They are case-sensitive: Age and age are different.
– Use meaningful names, like temperature, instead of t.
• Dynamic Typing: Python does not require you to declare the type. The same variable
can hold different types of data at different times:
x = 10 # integer
x = "hello" # now it's a string
• Swapping Values: Python makes it easy to swap values without a temporary variable:
a, b = b, a
Tiny Code
# Assign variables
name = "Ada"
age = 25
# Print them
print("My name is", name)
print("I am", age, "years old")
Why it Matters
Variables let you store and reuse information in your programs. Without variables, you would
have to repeat values everywhere, making your code harder to read and change. They are the
foundation of all programming.
Try It Yourself
1. Create a variable called color and assign your favorite color as text.
2. Make a variable number and assign it any number you like.
3. Print both values in a sentence, like:
12
4. Try changing the values and run the program again.
This will show you how variables make your code flexible and easy to update.
In Python, a variable is like a box with a name where you can store information. You can put
numbers, text, or other kinds of data inside that box, and later use the name of the box to get
the value back.
Unlike some languages, you don’t need to say what kind of data will go inside the box—Python
figures it out for you automatically.
Deep Dive
To create a variable, you simply choose a name and use the equals sign = to assign a value. For
example:
age = 20
name = "Alice"
height = 1.75
You can also change the value at any time. For instance:
Variable names have a few rules. They can include letters, numbers, and underscores (_), but
they cannot start with a number. They are also case-sensitive, so Age and age are considered
different. It’s a good habit to use meaningful names, like temperature instead of just t.
Python uses dynamic typing, which means you don’t have to declare the type of data in advance.
A single variable can hold different types of data at different times:
x = 10 # integer
x = "hello" # now it's a string
You can even assign several variables in one line, like this:
a, b, c = 1, 2, 3
13
And if you ever need to swap the values of two variables, Python makes it very easy without
needing a temporary helper:
a, b = b, a
Tiny Code
# Assign variables
name = "Ada"
age = 25
# Print them
print("My name is", name)
print("I am", age, "years old")
Why it Matters
Variables let you store and reuse information in your programs. Without variables, you would
have to repeat values everywhere, making your code harder to read and change. They are the
foundation of all programming.
Try It Yourself
1. Create a variable called color and assign your favorite color as text.
2. Make a variable number and assign it any number you like.
3. Print both values in a sentence, like:
This will show you how variables make your code flexible and easy to update.
Every piece of information in Python has a data type. A data type tells Python what kind of
thing the value is—whether it’s a number, text, a list of items, or something else. Understanding
data types is important because it helps you know what you can and cannot do with a value.
14
Deep Dive
Python has several basic data types you’ll use all the time.
Numbers are used for math. Python has three main kinds of numbers: integers (int) for whole
numbers, floating-point numbers (float) for decimals, and complex numbers (complex) which
are used less often, mostly in math and engineering.
Strings (str) represent text. Anything inside quotes, either single ('hello') or double
("hello"), is treated as a string. Strings can hold words, sentences, or even whole paragraphs.
Booleans (bool) represent truth values—either True or False. These are useful for decision
making in programs, like checking if a condition is met.
Collections let you store multiple values in a single variable. Lists (list) are ordered, changeable
collections of items, like [1, 2, 3]. Tuples (tuple) are like lists but cannot be changed after
creation, such as (1, 2, 3). Sets (set) are collections of unique, unordered items. Dictionaries
(dict) store data as key–value pairs, like {"name": "Alice", "age": 25}.
There are also special types like NoneType, which only has the value None. This represents
“nothing” or “no value.”
Python figures out the type of a variable automatically. If you want to check a variable’s type,
you can use the built-in type() function:
x = 42
print(type(x)) # <class 'int'>
Tiny Code
15
Why it Matters
Data types are the foundation of programming logic. Knowing the type of data tells you what
operations you can perform. For example, you can add two numbers but not a number and a
string without converting one of them. This prevents errors and helps you design programs
correctly.
Try It Yourself
This will give you a feel for how Python handles different data and why types matter.
Numbers are one of the most basic building blocks in Python. They allow you to do math,
represent quantities, and calculate results in your programs. Python has three main types of
numbers: integers (int), floating-point numbers (float), and complex numbers (complex).
Deep Dive
16
Operator Example Result Meaning
+ 5 + 2 7 Addition
- 5 - 2 3 Subtraction
* 5 * 2 10 Multiplication
/ 5 / 2 2.5 Division (always float)
// 5 // 2 2 Floor division (whole number part only)
% 5 % 2 1 Modulo (remainder)
‘|2 3|8‘ Exponent
(raise to a
power)
Type Conversion
You can check the type of any number with the type() function:
x = 42
print(type(x)) # <class 'int'>
Tiny Code
# Integers
a = 10
b = -3
# Floats
pi = 3.14
g = 9.81
# Complex
z = 2 + 3j
# Operations
print(a + b) # 7
17
print(a / 2) # 5.0
print(a // 2) # 5
print(a % 3) # 1
print(2 3) # 8
# Type checking
print(type(pi)) # <class 'float'>
print(type(z)) # <class 'complex'>
Why it Matters
Numbers are essential for everything from simple calculations to complex algorithms. Under-
standing the different numeric types and how they behave allows you to choose the right one
for each situation. Use integers for counting, floats for precise measurements, and complex
numbers for specialized scientific work.
Try It Yourself
1. Create two integers and try all the arithmetic operators (+, -, *, /, //, %, “).
2. Make a float variable for your height (like 1.75) and multiply it by 2.
3. Experiment with int(), float(), and complex() to convert between number types.
4. Write a complex number and print both its real and imaginary parts using .real and
.imag.
This will help you see how Python handles different numeric types in practice.
Deep Dive
Creating Strings
You can create strings using either single quotes or double quotes:
name = 'Alice'
greeting = "Hello, world!"
18
For multi-line text, you can use triple quotes:
paragraph = """This is a
multi-line string."""
Escape Characters
Sometimes you need special characters inside a string:
Tiny Code
# Creating strings
word = "Python"
sentence = 'I love coding'
multiline = """This is
a string that spans
multiple lines."""
# Operations
print(word[0]) # 'P'
print(word[-1]) # 'n'
print(word[0:3]) # 'Pyt'
19
print(word + " 3.12") # 'Python 3.12'
print("ha" * 4) # 'hahaha'
# Escape characters
path = "C:\\Users\\Alice"
print(path)
Why it Matters
Strings are everywhere—whether you’re printing messages, reading files, sending data across
the internet, or handling user input. Mastering how to create and manipulate strings is essential
for building real-world Python programs.
Try It Yourself
1. Create a string with your full name and print the first letter and the last letter.
2. Write a sentence and use slicing to print only the first 5 characters.
3. Use string concatenation to join "Hello" and your name with a space in between.
4. Make a string with an escape sequence, like "Line1\nLine2", and print it.
This practice will help you understand how Python treats text as data you can store, manipulate,
and display.
Booleans are the simplest type of data in Python. They represent only two values: True or
False. Booleans are often the result of comparisons or conditions in a program, and they
control the flow of logic, such as deciding which branch of an if statement should run.
Deep Dive
Boolean Values
In Python, the boolean type is bool. There are only two possible values:
is_sunny = True
is_raining = False
20
Notice that True and False are capitalized—writing true or false will cause an error.
Comparisons That Produce Booleans
Boolean Logic
Python also supports logical operators that combine boolean values:
Truthiness in Python
Not just True and False are considered booleans. Many values in Python have an implicit
boolean value:
print(bool(0)) # False
print(bool("hi")) # True
21
Tiny Code
x = 10
y = 20
# Truthiness
print(bool("")) # False
print(bool("Python")) # True
Why it Matters
Booleans are the foundation of decision-making in programming. They let you write programs
that can react differently depending on conditions—like checking if a user is logged in, if there
is enough money in a bank account, or if a file exists. Without booleans, all programs would
just run straight through without making choices.
Try It Yourself
This practice will help you see how conditions and logic form the backbone of Python pro-
grams.
9. Comments in Python
Comments are notes you add to your code that Python ignores when running the program.
They’re meant for humans, not the computer. Comments explain what your code does, why
you wrote it a certain way, or leave reminders for yourself and others.
22
Deep Dive
Single-Line Comments In Python, the # symbol marks the start of a comment. Everything
after it on the same line is ignored by Python:
Multi-Line Comments (Docstrings) Python doesn’t have a special syntax just for multi-line
comments, but programmers often use triple quotes (""" or '''). These are usually used for
docstrings (documentation strings), but they can serve as block comments if not assigned to a
variable:
"""
This is a multi-line comment.
You can use triple quotes
to write long explanations.
"""
Docstrings for Functions and Classes Triple quotes are more commonly used as docstrings to
document functions, classes, or modules. They are placed right after the definition line:
def greet(name):
"""
This function takes a name
and prints a greeting.
"""
print("Hello,", name)
Purpose Example
Explain code logic # Loop through items in the list
Clarify tricky parts # Using floor division to ignore decimals
Leave reminders (TODOs, # TODO: handle negative numbers
FIXMEs)
Provide documentation Docstrings that explain functions, classes, or entire files
Good comments don’t just repeat the code; they explain the why, not just the what.
23
Tiny Code
def square(x):
"""Return the square of a number."""
return x * x
print(square(4)) # prints 16
Why it Matters
Comments make your code easier to understand for both yourself and others. Six months from
now, you might forget why you wrote something. Clear comments act like a guidebook. In
teams, comments and docstrings are essential for collaboration, as they make the codebase
easier to maintain.
Try It Yourself
1. Write a small program that calculates the area of a rectangle. Add comments explaining
what each step does.
2. Use a triple-quoted docstring to describe what the whole program does at the top of your
file.
3. Add a TODO comment to remind yourself to improve the program later (for example,
adding user input).
This will show you how comments make programs not just for computers, but for people too.
The print() function is one of the most commonly used tools in Python. It lets you display
information on the screen so you can see the result of your program, check values, or interact
with users.
24
Deep Dive
print("Hello, world!")
Printing Variables You can print variables directly by passing them to print():
name = "Ada"
age = 25
print(name)
print(age)
Printing Multiple Values print() can take multiple arguments separated by commas. Python
will add spaces between them automatically:
String Formatting There are several ways to make your output more readable:
End and Separator Options By default, print() ends with a new line (\n). You can change
this using the end parameter:
You can also change the separator between multiple items using sep:
Printing Special Characters You can print new lines or tabs with escape sequences:
25
print("Line1\nLine2")
print("A\tB")
Tiny Code
name = "Grace"
language = "Python"
year = 1991
print("Hello, world!")
print("My name is", name)
print(f"{name} created {language} in {year}?")
print("apple", "orange", "grape", sep=" | ")
Why it Matters
Printing is the most direct way to see what your program is doing. It helps you understand
results, debug mistakes, and communicate with users. Even professional developers rely heavily
on print() when testing and exploring code quickly.
Try It Yourself
This will show you how flexible print() is for displaying information in Python.
Comparison operators let you compare two values and return a boolean result (True or False).
They are the foundation for making decisions in Python programs—without them, you couldn’t
check conditions like “Is this number bigger than that number?” or “Are these two things
equal?”
26
Deep Dive
Comparison operators work on numbers, strings, and many other types. They allow you to
check equality, inequality, and order.
Basic Comparison Operators
Comparisons always return True or False, which can be stored in variables or used directly
inside control flow statements (if, while).
Chained Comparisons Python allows chaining comparisons for readability:
x = 5
print(1 < x < 10) # True
print(10 < x < 20) # False
Tiny Code
x = 10
y = 20
print(x == y) # False
print(x != y) # True
27
print(x > y) # False
print(x <= y) # True
# Chain comparisons
print(5 < x < 15) # True
Why it Matters
Without comparisons, programs couldn’t make choices. They are the basis for decisions like
checking passwords, validating input, controlling loops, or comparing values in data. Every
real-world Python program relies on comparison operators to “decide what to do next.”
Try It Yourself
1. Write a program that compares two numbers (a = 7, b = 12) and prints whether a is
less than, greater than, or equal to b.
2. Create two strings and check if they are equal.
3. Use a chained comparison to check if a number n = 15 is between 10 and 20.
4. Experiment with < and > on strings like "cat" and "dog" to see how Python compares
text.
Logical operators combine boolean values (True or False) to form more complex conditions.
They are essential when you want to check multiple things at once, like “Is the number positive
and even?” or “Is this user an admin or a guest?”
Deep Dive
Truth Tables
and operator:
28
A B A and B
True True True
True False False
False True False
False False False
or operator:
A B A or B
True True True
True False True
False True True
False False False
not operator:
A not A
True False
False True
age = 20
is_student = True
• For and, if the first condition is False, Python won’t check the second.
• For or, if the first condition is True, Python won’t check the second.
Tiny Code
29
x = 10
y = 5
# Short-circuit example
print(False and (10/0)) # False, no error (second part skipped)
print(True or (10/0)) # True, no error (second part skipped)
Why it Matters
Logical operators allow your programs to make more complex decisions by combining multiple
conditions. They’re at the heart of all real-world logic, from validating form inputs to controlling
access in applications.
Try It Yourself
1. Write a condition that checks if a number is both positive and less than 100.
2. Check if a variable name is either "Alice" or "Bob".
3. Use not to test if a list is empty (not my_list).
4. Experiment with short-circuiting by combining and or or with expressions that would
normally cause an error.
13. if Statements
An if statement lets your program make decisions. It checks a condition, and if that condition
is True, it runs a block of code. If the condition is False, the block is skipped. This is the
most basic form of control flow in Python.
Deep Dive
Basic Structure
if condition:
# code runs only if condition is True
30
The colon (:) signals the start of the block, and indentation shows which lines belong to the
if.
Example
x = 10
if x > 5:
print("x is greater than 5")
Indentation is Required All code inside the if block must be indented the same amount.
Without correct indentation, Python will raise an IndentationError.
Tiny Code
temperature = 30
if temperature < 0:
print("It's freezing!")
Why it Matters
Without if statements, programs would always run the same way. Conditions make programs
dynamic and responsive—whether it’s checking user input, validating data, or making choices
in games, if is the starting point for logic in Python.
31
Try It Yourself
14. if...else
The if...else structure lets your program choose between two paths. If the condition is True,
the if block runs. If the condition is False, the else block runs instead. This ensures that
one of the two blocks always executes.
Deep Dive
Basic Structure
if condition:
# code runs if condition is True
else:
# code runs if condition is False
Example
age = 16
Here, if age is 18 or more, the first message is printed. Otherwise, the second one runs.
if...else with Variables You can use the result of conditions to assign values:
x = 10
y = 20
32
This is called a ternary expression (or conditional expression).
Only One else An if statement can have at most one else, and it always comes last.
Tiny Code
score = 75
Why it Matters
The if...else structure makes programs capable of handling two outcomes: one when a
condition is met, and another when it isn’t. It’s essential for branching logic—without it, you
could only run code when conditions are true, not handle the “otherwise” case.
Try It Yourself
15. if...elif...else
The if...elif...else structure lets you check multiple conditions in order. The program
will run the first block where the condition is True, and then skip the rest. If none of the
conditions are true, the else block runs.
Deep Dive
Basic Structure
33
if condition1:
# runs if condition1 is True
elif condition2:
# runs if condition1 is False AND condition2 is True
elif condition3:
# runs if above are False AND condition3 is True
else:
# runs if none of the above are True
Example
score = 85
Here, Python checks each condition in order. Since score >= 75 is true, it prints "Good" and
skips the rest.
Order Matters Conditions are checked from top to bottom. As soon as one is True, Python
stops checking further. For example:
x = 100
if x > 50:
print("Bigger than 50")
elif x > 10:
print("Bigger than 10")
Only "Bigger than 50" is printed, even though x > 10 is also true.
Optional Parts
34
Tiny Code
day = "Wednesday"
if day == "Monday":
print("Start of the week")
elif day == "Friday":
print("Almost weekend")
elif day == "Saturday" or day == "Sunday":
print("Weekend!")
else:
print("Midweek day")
Why it Matters
Most real-life decisions aren’t just yes-or-no. The if...elif...else chain lets you handle
multiple possibilities in an organized way, making your code more flexible and readable.
Try It Yourself
1. Write a program that checks a number and prints "Positive", "Negative", or "Zero".
2. Create a grading system: 90+ = A, 75–89 = B, 60–74 = C, below 60 = F.
3. Write code that prints which day of the week it is, based on a variable day.
4. Experiment by changing the order of conditions and observe how the output changes.
A nested condition means putting one if statement inside another. This allows your program
to make more specific decisions by checking an additional condition only when the first one is
true.
Deep Dive
Basic Structure
35
if condition1:
if condition2:
# runs if both condition1 and condition2 are True
else:
# runs if condition1 is True but condition2 is False
else:
# runs if condition1 is False
Example
age = 20
is_student = True
Here, the second check (is_student) only happens if the first check (age >= 18) is true.
Why Nesting is Useful Nested conditions let you handle cases that depend on multiple layers of
logic. However, too much nesting can make code hard to read. In such cases, logical operators
(and, or) are often better:
Best Practice
• Use nesting when the second condition should only be checked if the first one is true.
• For readability, avoid deep nesting—prefer combining conditions with logical operators
when possible.
Tiny Code
36
x = 15
if x > 0:
if x % 2 == 0:
print("Positive even number")
else:
print("Positive odd number")
else:
print("Zero or negative number")
Why it Matters
Nested conditions add depth to decision-making. They let you structure logic in layers, which
is closer to how we reason in real life—for example, “If the shop is open, then check if I have
enough money.”
Try It Yourself
1. Write a program that checks if a number is positive. If it is, then check if it’s greater
than 100.
2. Make a program that checks if someone is eligible to drive: first check if age >= 18, then
check if has_license == True.
3. Rewrite one of your nested conditions using and instead, and compare which version is
easier to read.
A while loop lets your program repeat a block of code as long as a condition is True. It’s useful
when you don’t know in advance how many times you need to loop—for example, waiting for
user input or running until some condition changes.
Deep Dive
Basic Structure
while condition:
# code runs as long as condition is True
Example
37
count = 1
while count <= 5:
print("Count is:", count)
count += 1
This loop prints numbers from 1 to 5. Each time, count increases by 1 until the condition
count <= 5 is no longer true.
Infinite Loops If the condition never becomes False, the loop will run forever. For example:
while True:
print("This never ends!")
x = 0
while x < 10:
if x == 5:
break
print(x)
x += 1
Using continue to Skip The continue keyword skips to the next iteration without finishing
the rest of the loop body.
Common Use Cases
Tiny Code
38
Why it Matters
The while loop gives your program flexibility to keep running until something changes. It’s
a powerful way to model “keep doing this until…” logic that often appears in real-world
problems.
Try It Yourself
A for loop in Python is used to repeat a block of code a specific number of times. Unlike the
while loop, which runs as long as a condition is true, the for loop usually goes through a
sequence of values—often created with the built-in range() function.
Deep Dive
Basic Structure
Examples:
for i in range(5):
print(i) # 0, 1, 2, 3, 4
39
for i in range(0, 10, 2):
print(i) # 0, 2, 4, 6, 8
Looping with else A for loop can have an optional else block that runs if the loop finishes
normally (not stopped by break).
for i in range(3):
print(i)
else:
print("Loop finished")
Common Patterns
Tiny Code
Why it Matters
The for loop is the most common way to repeat actions in Python when you know how many
times to loop. It’s simpler and clearer than a while loop for counting and iterating over
ranges.
Try It Yourself
40
19. Loop Control (break, continue)
Sometimes you need more control over loops. Python provides two special keywords—break
and continue—to change how a loop behaves. These allow you to stop a loop early or skip
parts of it.
Deep Dive
break — Stop the Loop The break statement ends the loop immediately, even if the loop
condition or range still has more values.
for i in range(10):
if i == 5:
break
print(i)
# Output: 0, 1, 2, 3, 4
continue — Skip to Next Iteration The continue statement skips the rest of the loop body
and moves to the next iteration.
for i in range(5):
if i == 2:
continue
print(i)
# Output: 0, 1, 3, 4
Using with while Loops Both break and continue work the same way in while loops.
x = 0
while x < 5:
x += 1
if x == 3:
continue
if x == 5:
break
print(x)
# Output: 1, 2, 4
When to Use
• break is useful when you find what you’re looking for and don’t need to continue looping.
• continue is useful when you want to skip over certain cases but still keep looping.
41
Tiny Code
Why it Matters
Without loop control, you would have to add extra complicated logic or duplicate code. break
and continue give you fine-grained control, making loops cleaner, more efficient, and easier to
understand.
Try It Yourself
1. Write a loop that prints numbers from 1 to 100, but stops when it reaches 42.
2. Write a loop that prints numbers from 1 to 10, but skips multiples of 3.
3. Combine both: loop through numbers 1 to 20, skip evens, and stop completely if you
find 15.
In Python, a for or while loop can have an optional else block. The else part runs only if
the loop finishes normally—that is, it isn’t stopped early by a break. This feature is unique to
Python and is often used when searching for something.
Deep Dive
Basic Structure
42
for item in sequence:
# loop body
else:
# runs if loop finishes without break
for i in range(5):
print(i)
else:
print("Loop finished")
for i in range(5):
if i == 3:
break
print(i)
else:
print("Finished without break")
# Output: 0, 1, 2
x = 0
while x < 3:
print(x)
x += 1
else:
print("While loop ended normally")
Practical Use Case: Searching The else block is handy when searching for an item. If you find
the item, break ends the loop; if not, the else runs.
numbers = [1, 2, 3, 4, 5]
for n in numbers:
if n == 7:
print("Found 7!")
43
break
else:
print("7 not found")
Tiny Code
Why it Matters
The else clause on loops lets you handle the “nothing found” case cleanly without needing
extra flags or checks. It makes code shorter and easier to understand when searching or checking
conditions across a loop.
Try It Yourself
1. Write a loop that searches for the number 10 in a list of numbers. If found, print "Found".
If not, let the else print "Not found".
2. Create a while loop that counts from 1 to 5 and uses an else block to print "Done
counting".
3. Experiment with adding break inside your loop to see how it changes whether the else
runs.
A list in Python is an ordered collection of items. Think of it like a container where you can
store multiple values in a single variable—numbers, strings, or even other lists. Lists are one of
the most commonly used data structures in Python because they’re flexible and easy to work
with.
44
Deep Dive
Creating Lists You create a list by putting values inside square brackets [], separated by
commas:
empty = []
Lists Are Ordered The items keep the order you put them in. If you create a list [10, 20,
30], Python remembers that order unless you change it.
Lists Can Be Changed (Mutable) Unlike strings or tuples, lists can be modified after creation—
you can add, remove, or replace elements.
Accessing Elements Each item in a list has an index (position), starting at 0:
Length of a List You can find out how many items a list has with len():
print(len(fruits)) # 3
Tiny Code
45
colors = ["red", "green", "blue"]
Why it Matters
Lists let you store and organize multiple values in one place. Without lists, you’d need a
separate variable for each value, which quickly becomes messy. Lists are the foundation for
handling collections of data in Python.
Try It Yourself
Lists in Python are ordered, which means each item has a position (index). You can use indexes
to get specific elements, or slices to get parts of the list.
Deep Dive
print(fruits[-1]) # "date"
print(fruits[-2]) # "cherry"
Slicing Basics Slicing lets you grab a portion of a list. The syntax is:
46
list[start:stop]
If you leave out start, Python begins at the start of the list:
Slicing with Step You can add a third number for step size:
numbers = [0, 1, 2, 3, 4, 5]
print(numbers[::2]) # [0, 2, 4]
print(numbers[1::2]) # [1, 3, 5]
print(numbers[::-1]) # [5, 4, 3, 2, 1, 0]
Tiny Code
47
colors = ["red", "green", "blue", "yellow"]
print(colors[0]) # red
print(colors[-1]) # yellow
print(colors[1:3]) # ['green', 'blue']
print(colors[::-1]) # ['yellow', 'blue', 'green', 'red']
Why it Matters
Indexing and slicing make it easy to get exactly the parts of a list you need. Whether you’re
grabbing one item, a range of items, or reversing the list, these tools are essential for working
with collections of data.
Try It Yourself
1. Make a list of 6 numbers and print the first, third, and last elements.
2. Slice your list to get the middle three elements.
3. Use slicing with a step of 2 to get every other number.
4. Reverse the list using slicing and print the result.
Lists in Python come with built-in methods that make it easy to add, remove, and modify
items. These methods are powerful tools for managing collections of data.
Deep Dive
Adding Items
Removing Items
48
• remove(x) → removes the first occurrence of x.
• pop(i) → removes and returns the item at index i (defaults to last).
• clear() → removes all items.
nums = [1, 2, 2, 3]
print(nums.index(2)) # 1
print(nums.count(2)) # 2
49
Tiny Code
colors.append("green")
colors.extend(["yellow", "purple"])
colors.insert(2, "orange")
colors.remove("blue")
last = colors.pop()
print(last) # 'purple'
print(colors.count("red")) # 1
colors.sort()
print(colors) # ['green', 'orange', 'red', 'yellow']
Why it Matters
List methods are essential for real-world programming, where data is always changing. Being
able to add, remove, and reorder items makes lists versatile tools for tasks like managing to-do
lists, processing datasets, or handling user inputs.
Try It Yourself
1. Start with a list of three numbers. Add two more using append() and extend().
2. Insert a number at the beginning of the list.
3. Remove one number using remove(), then use pop() to remove the last one.
4. Sort your list and then reverse it. Print the result at each step.
24. Tuples
A tuple is an ordered collection of items, just like a list, but with one big difference: tuples are
immutable. This means once you create a tuple, you cannot change its contents—no adding,
removing, or modifying items. Tuples are useful when you want to store data that should not
be altered.
50
Deep Dive
Creating Tuples You create a tuple using parentheses () instead of square brackets:
numbers = (1, 2, 3)
fruits = ("apple", "banana", "cherry")
single = (5,)
print(type(single)) # <class 'tuple'>
Accessing Elements Tuples use the same indexing and slicing as lists:
print(fruits[0]) # "apple"
print(fruits[-1]) # "cherry"
print(fruits[0:2]) # ("apple", "banana")
Tuple Packing and Unpacking You can pack multiple values into a tuple and unpack them into
variables:
point = (3, 4)
x, y = point
print(x, y) # 3 4
Use Cases
51
Quick Summary Table
Tiny Code
print(colors[1]) # green
print(len(colors)) # 3
# Unpacking
r, g, b = colors
print(r, b) # red blue
# Methods
print(colors.index("blue")) # 2
print(colors.count("red")) # 1
Why it Matters
Tuples give you a safe way to group data that should not be changed, protecting against
accidental modifications. They are also slightly faster than lists, making them useful when
performance matters and immutability is desired.
Try It Yourself
1. Create a tuple with three of your favorite foods and print the second one.
2. Try changing one element—observe the error.
3. Use unpacking to assign a tuple (10, 20, 30) into variables a, b, c.
4. Create a dictionary where the key is a tuple of coordinates (x, y) and the value is a
place name.
52
25. Sets
A set in Python is an unordered collection of unique items. Sets are useful when you need to
store data without duplicates or when you want to perform mathematical operations like union
and intersection.
Deep Dive
Creating Sets You can create a set using curly braces {} or the set() function:
Unordered Sets do not preserve order. You cannot access elements by index (set[0] �).
Adding and Removing Items
s = {1, 2}
s.add(3) # {1, 2, 3}
s.update([4, 5]) # {1, 2, 3, 4, 5}
s.remove(2) # {1, 3, 4, 5}
s.discard(10) # no error
53
a = {1, 2, 3}
b = {3, 4, 5}
Tiny Code
numbers = {1, 2, 3, 3, 2}
print(numbers) # {1, 2, 3}
numbers.add(4)
numbers.discard(1)
print(numbers) # {2, 3, 4}
odds = {1, 3, 5}
evens = {2, 4, 6}
print(odds | evens) # {1, 2, 3, 4, 5, 6}
Why it Matters
Sets make it easy to eliminate duplicates and perform operations like union or intersection,
which are common in data analysis, algorithms, and everyday programming tasks. They are
also optimized for fast membership testing.
54
Try It Yourself
Sets in Python shine when you use them for mathematical-style operations. They let you
combine, compare, and filter items in powerful ways. These operations are very fast and are
often used in data processing, searching, and analysis.
Deep Dive
Union (| or .union()) The union of two sets contains all unique items from both.
a = {1, 2, 3}
b = {3, 4, 5}
print(a | b) # {1, 2, 3, 4, 5}
print(a.union(b)) # {1, 2, 3, 4, 5}
Intersection (& or .intersection()) The intersection contains only items present in both
sets.
Difference (- or .difference()) The difference contains items in the first set but not the
second.
print(a - b) # {1, 2}
print(b - a) # {4, 5}
print(a ^ b) # {1, 2, 4, 5}
print(a.symmetric_difference(b)) # {1, 2, 4, 5}
55
• a <= b → checks if a is a subset of b.
• a >= b → checks if a is a superset of b.
x = {1, 2}
y = {1, 2, 3}
print(x <= y) # True (x is subset of y)
print(y >= x) # True (y is superset of x)
Sym- Exam-
Operation bol ple Result
Union ‘ ‘ ‘a b‘ all unique items
Intersection & a & b common items
Difference - a - b in a not b
Symmetric difference ^ a ^ b in a or b, not both
Subset <= a <= b True/False
Superset >= a >= b True/False
Tiny Code
a = {1, 2, 3}
b = {3, 4, 5}
print("Union:", a | b) # {1, 2, 3, 4, 5}
print("Intersection:", a & b) # {3}
print("Difference:", a - b) # {1, 2}
print("SymDiff:", a ^ b) # {1, 2, 4, 5}
Why it Matters
Set operations allow you to quickly solve problems like finding common elements, removing
duplicates, or checking membership across collections. They map directly to real-world logic
such as “all users,” “users in both groups,” or “items missing from one list.”
56
Try It Yourself
1. Make two sets of numbers: {1, 2, 3, 4} and {3, 4, 5, 6}. Find their union, inter-
section, and difference.
2. Create a set of vowels and a set of letters in the word "python". Find the intersection to
see which vowels appear.
3. Check if {1, 2} is a subset of {1, 2, 3, 4}.
4. Try symmetric difference between {a, b, c} and {b, c, d}.
Deep Dive
Creating Dictionaries You create a dictionary using curly braces {} with keys and values
separated by colons:
print(person["name"]) # "Alice"
print(person["age"]) # 25
Adding and Updating Dictionaries are mutable—you can add new key–value pairs or update
existing ones:
person["job"] = "Engineer"
person["age"] = 26
Keys Must Be Unique If you repeat a key, the latest value will overwrite the earlier one:
57
• Keys must be immutable types (strings, numbers, tuples).
• Values can be any type: strings, numbers, lists, or even other dictionaries.
empty = {}
Tiny Code
print(car["brand"]) # Toyota
car["year"] = 2021 # update value
car["color"] = "blue" # add new key
print(car)
Why it Matters
Dictionaries give you a natural way to organize and retrieve data by name instead of position.
They are essential for representing structured data, like database records, configurations, or
JSON data from APIs.
Try It Yourself
1. Create a dictionary called student with keys "name", "age", and "grade".
2. Access and print the "grade".
3. Update the "age" to a new number.
4. Add a new key "passed" with the value True.
5. Print the whole dictionary to see the changes.
58
28. Dictionary Methods
Dictionaries come with built-in methods that make it easy to work with their keys and values.
These methods let you add, remove, and inspect data in a structured way.
Deep Dive
Removing Items
print(person.pop("age")) # 26
print(person.popitem()) # ('city', 'Paris')
del person["name"] # removes "name"
person.clear() # {}
• get(key, default) → safely gets a value; returns default if the key doesn’t exist.
59
person = {"name": "Alice"}
print(person.get("age", "Not found")) # "Not found"
From Keys
• dict.fromkeys(keys, value) → creates a dictionary with given keys and default value.
Tiny Code
student.update({"age": 21})
print(student)
student.pop("grade")
print(student)
60
Why it Matters
Dictionary methods let you manipulate structured data efficiently. Whether you’re cleaning
data, merging information, or safely handling missing values, these methods are essential for
working with real-world datasets and configurations.
Try It Yourself
A nested structure means putting one data structure inside another—for example, a list of lists,
a dictionary containing lists, or even a list of dictionaries. Nested structures are common when
representing more complex, real-world data.
Deep Dive
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(matrix[0][1]) # 2
student = {
"name": "Alice",
"grades": [85, 90, 92]
}
print(student["grades"][1]) # 90
Lists of Dictionaries A list can contain multiple dictionaries, useful for structured records:
61
people = [
{"name": "Alice", "age": 25},
{"name": "Bob", "age": 30}
]
print(people[1]["name"]) # Bob
users = {
"alice": {"age": 25, "city": "Paris"},
"bob": {"age": 30, "city": "London"}
}
print(users["bob"]["city"]) # London
Iteration Through Nested Structures You can use loops inside loops to navigate deeper levels:
Tiny Code
classrooms = {
"A": ["Alice", "Bob"],
"B": ["Charlie", "Diana"]
}
62
Why it Matters
Real-world data is rarely flat—it’s often hierarchical or structured in layers (like JSON from
APIs, database rows with embedded fields, or spreadsheets). Nested structures let you represent
and work with this complexity directly in Python.
Try It Yourself
1. Create a list of lists to represent a 3×3 grid and print the center value.
2. Make a dictionary with a key "friends" pointing to a list of three names. Print the
second name.
3. Create a list of dictionaries, each with "title" and "year", for your favorite movies.
Print the title of the last one.
4. Build a dictionary of dictionaries representing two countries with their capital cities, then
print one capital.
Chapter 4. Functions
A function is a reusable block of code that performs a specific task. Functions let you avoid
repetition, organize your code, and make programs easier to understand. In Python, you define
a function using the def keyword.
Deep Dive
def greet():
print("Hello!")
def greet(name):
print("Hello,", name)
63
Return Values Functions can return data with return:
result = add(3, 4)
print(result) # 7
Default Behavior
Tiny Code
def square(n):
return n * n
print(square(5)) # 25
def welcome(name):
print("Welcome,", name)
64
Why it Matters
Functions are the building blocks of programs. They let you break down complex problems
into smaller pieces, reuse code efficiently, and make your programs easier to maintain and
understand.
Try It Yourself
Functions can take arguments (also called parameters) so you can pass information into them.
Arguments make functions flexible because they can work with different inputs instead of being
hardcoded.
Deep Dive
Positional Arguments The most common type—values are matched to parameters in order.
greet("Alice", 25)
Keyword Arguments You can pass values by naming the parameters. This makes the call
clearer and order doesn’t matter.
greet(age=30, name="Bob")
Default Arguments You can give parameters default values, making them optional when calling
the function.
65
def greet(name, age=18):
print("Hello,", name, "you are", age)
Mixing Arguments When mixing, positional arguments come first, then keyword arguments.
Tiny Code
66
Why it Matters
Arguments let you write one function that works in many situations. Instead of duplicating
code, you can pass in different values and reuse the same function. This is one of the core ideas
of programming.
Try It Yourself
Python functions can define default values for parameters and accept keyword arguments when
called. These features make functions flexible and easier to use by reducing how much you
need to type and improving readability.
Deep Dive
Default Arguments When defining a function, you can give a parameter a default value. If the
caller doesn’t provide it, Python uses the default.
def greet(name="Friend"):
print("Hello,", name)
Multiple Defaults You can set defaults for more than one parameter.
67
Keyword Arguments When calling a function, you can use parameter names. This makes it
clear what each value means, and order doesn’t matter.
Mixing Positional and Keyword Arguments You can mix both, but positional arguments must
come first.
Important Rule Default arguments are evaluated once, when the function is defined. Be careful
with mutable defaults like lists or dictionaries—they can persist changes between calls.
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ← reused same list!
68
Feature Example Benefit
Mixing positional+keyword f(1, y=2) Flexible calls
Mutable default trap def f(lst=[]) Avoid with None as default
Tiny Code
greet()
greet("Alice")
greet("Bob", lang="fr")
Why it Matters
Default and keyword arguments make functions more user-friendly. They reduce repetitive
code, prevent errors from missing values, and improve readability when functions have many
parameters.
Try It Yourself
1. Write a function multiply(a, b=2) that returns a * b. Call it with one argument and
with two.
2. Create a function profile(name, age=18, city="Unknown") and call it using keyword
arguments in any order.
3. Test the mutable default trap by defining a function with list=[]. See how it behaves
after multiple calls.
4. Rewrite it using None as the default and verify the issue is fixed.
69
34. Return Values
Functions don’t just perform actions—they can also send results back using the return
statement. This makes functions powerful, because you can store their output, use it in
calculations, or pass it into other functions.
Deep Dive
Basic Return
result = add(3, 4)
print(result) # 7
When Python hits return, the function stops and sends the value back.
Returning Multiple Values Python functions can return more than one value by returning a
tuple:
def get_stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
def say_hello():
print("Hello")
result = say_hello()
print(result) # None
Return vs Print
70
def square(x):
return x * x
Tiny Code
def cube(n):
return n 3
def min_max(nums):
return min(nums), max(nums)
print(cube(4)) # 64
low, high = min_max([3, 7, 2, 9])
print(low, high) # 2 9
Why it Matters
Return values make functions reusable building blocks. Instead of just displaying results,
functions can calculate and hand back values, letting you compose larger programs from smaller
pieces.
71
Try It Yourself
In Python, scope refers to where a variable can be accessed in your code. Variables created inside
a function exist only there, while variables created outside are available globally. Understanding
scope helps avoid bugs and keeps code organized.
Deep Dive
Local Variables A variable created inside a function is local to that function. It only exists
while the function runs.
def greet():
message = "Hello" # local variable
print(message)
greet()
# print(message) � Error: message not defined
Global Variables A variable created outside functions is global and can be used anywhere.
def say_name():
print("My name is", name)
Local vs Global Priority If a local variable has the same name as a global one, Python uses the
local one inside the function.
72
x = 10 # global
def show():
x = 5 # local
print(x)
show() # 5
print(x) # 10
Using global Keyword If you want to modify a global variable inside a function, use global.
count = 0
def increase():
global count
count += 1
increase()
print(count) # 1
Best Practice
• Use local variables whenever possible—they are safer and easier to manage.
• Avoid modifying global variables inside functions unless absolutely necessary.
Tiny Code
x = 100 # global
def test():
x = 50 # local
73
print("Inside function:", x)
test()
print("Outside function:", x)
Why it Matters
Scope controls variable visibility and prevents accidental overwriting of values. By understanding
local vs global variables, you can write cleaner, more reliable code that avoids confusing bugs.
Try It Yourself
1. Create a global variable city = "Paris" and write a function that prints it.
2. Define a function with a local variable city = "London" and see which value prints
inside vs outside.
3. Make a counter using a global variable and a function that increases it with the global
keyword.
4. Write two functions that each define their own local variable with the same name, and
confirm they don’t affect each other.
In Python, functions can accept a flexible number of arguments using *args and kwargs. These
let you handle situations where you don’t know in advance how many inputs the user will
provide.
Deep Dive
def add_all(*args):
print(args)
add_all(1, 2, 3) # (1, 2, 3)
74
def add_all(*args):
return sum(args)
print(add_all(1, 2, 3, 4)) # 10
def show_info(kwargs):
print(kwargs)
show_info(name="Alice", age=25)
# {'name': 'Alice', 'age': 25}
def show_info(kwargs):
for key, value in kwargs.items():
print(key, "=", value)
show_info(city="Paris", country="France")
Combining *args and kwargs You can use both in the same function, but *args must come
before kwargs.
Unpacking with * and You can also use `*` and to unpack lists/tuples and dictionaries
into arguments.
75
nums = [1, 2, 3]
print(add_all(*nums)) # 6
Tiny Code
Why it Matters
*args and kwargs make functions more flexible and reusable. They let you handle unknown
numbers of inputs, write cleaner APIs, and pass around configurations easily.
Try It Yourself
76
37. Lambda Functions
A lambda function is a small, anonymous function defined with the keyword lambda. Unlike
normal functions defined with def, lambda functions are written in a single line and don’t
need a name unless you assign them to a variable. They’re often used for quick, throwaway
functions.
Deep Dive
Basic Syntax
Example:
square = lambda x: x * x
print(square(5)) # 25
Multiple Arguments
add = lambda a, b: a + b
print(add(3, 4)) # 7
No Arguments
Use with Built-in Functions Lambdas are often used with map(), filter(), and sorted().
nums = [1, 2, 3, 4]
squares = list(map(lambda x: x * x, nums))
print(squares) # [1, 4, 9, 16]
77
evens = list(filter(lambda x: x % 2 == 0, nums))
print(evens) # [2, 4]
Limitations
Tiny Code
78
Why it Matters
Lambda functions let you write short, inline functions without cluttering your code. They’re
especially handy for quick data transformations, sorting, and filtering when defining a full
function would be unnecessary.
Try It Yourself
38. Docstrings
Deep Dive
Basic Function Docstring Docstrings are written using triple quotes (""" ... """ or ''' ...
''') right below the function definition:
def greet(name):
"""Return a greeting message for the given name."""
return "Hello, " + name
print(greet.__doc__)
help(greet)
79
def add(a, b):
"""
Add two numbers and return the result.
Parameters:
a (int or float): First number.
b (int or float): Second number.
Returns:
int or float: The sum of a and b.
"""
return a + b
• For classes:
class Person:
"""A simple class representing a person."""
def __init__(self, name):
self.name = name
"""
This module provides math helper functions
like factorial and Fibonacci.
"""
80
Tiny Code
def factorial(n):
"""Calculate the factorial of n using recursion."""
return 1 if n == 0 else n * factorial(n - 1)
print(factorial.__doc__)
Why it Matters
Docstrings turn your code into self-documenting programs. They help others (and your future
self) understand how functions, classes, and modules should be used without reading all the code.
Tools like Sphinx and IDEs also use docstrings to generate documentation automatically.
Try It Yourself
A recursive function is a function that calls itself in order to solve a problem. Recursion is
useful when a problem can be broken down into smaller, similar subproblems—like calculating
factorials, traversing trees, or solving puzzles.
Deep Dive
81
def countdown(n):
if n == 0: # base case
print("Done!")
else:
print(n)
countdown(n - 1) # recursive case
def factorial(n):
if n == 0: # base case
return 1
return n * factorial(n - 1) # recursive case
print(factorial(5)) # 120
Example 2: Fibonacci Sequence Each Fibonacci number is the sum of the previous two.
def fib(n):
if n <= 1: # base case
return n
return fib(n - 1) + fib(n - 2)
print(fib(6)) # 8
Potential Issues
• Infinite recursion: forgetting a base case causes the function to call itself forever, leading
to an error (RecursionError).
• Performance: recursion can be slower and use more memory than loops for large inputs.
82
Tiny Code
def sum_list(numbers):
if not numbers: # base case
return 0
return numbers[0] + sum_list(numbers[1:]) # recursive case
print(sum_list([1, 2, 3, 4])) # 10
Why it Matters
Recursive functions let you write elegant, natural solutions to problems that involve repetition
with smaller pieces—like mathematical sequences, hierarchical data, or divide-and-conquer
algorithms.
Try It Yourself
A higher-order function is a function that either takes another function as an argument, returns
a function, or both. This makes Python very powerful for writing flexible and reusable code.
Deep Dive
Functions as Arguments Since functions are objects in Python, you can pass them around like
variables.
def square(n):
return n * n
print(apply_twice(square, 2)) # 16
83
Functions Returning Functions A function can also create and return another function.
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
print(double(5)) # 10
nums = [1, 2, 3]
squares = list(map(lambda x: x * x, nums))
print(squares) # [1, 4, 9]
• filter(func, iterable) → keeps only items where the function returns True.
84
Feature Example Purpose
Function as argument apply_twice(square, 2) Pass function in
Function as return value make_multiplier(3) Generate new function
map() map(lambda x:x+1, [1,2]) Apply function to items
filter() filter(lambda x:x>2, Keep items meeting condition
[1,2,3])
sorted(..., key=func) sorted(words, key=len) Custom sorting
reduce() reduce(lambda a,b:a*b, Accumulate values
nums)
Tiny Code
def shout(text):
return text.upper()
def whisper(text):
return text.lower()
speak(shout, "Hello")
speak(whisper, "Hello")
Why it Matters
Higher-order functions let you treat behavior as data. Instead of hardcoding actions, you can
pass in functions to customize behavior. This leads to more flexible, reusable, and expressive
programs.
Try It Yourself
1. Write a function apply(func, values) that applies func to every item in values (like
your own map).
2. Use filter() with a lambda to keep only numbers greater than 10 from a list.
3. Write a make_adder(n) function that returns a new function adding n to its input.
4. Use reduce() to calculate the sum of a list of numbers.
85
Chapter 5. Modules and Packages
A module in Python is a file containing Python code (functions, classes, variables) that you
can reuse in other programs. Importing a module lets you use its code without rewriting it.
Deep Dive
Basic Import Use the import keyword followed by the module name:
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.14159...
print(math.factorial(5)) # 120
Import Once Only A module is loaded once per program run, even if imported multiple times.
Where Python Looks for Modules
import sys
print(sys.path)
86
Quick Summary Table
Statement Meaning
import math Import the whole module
math.sqrt(25) Access function using module.function
import a, b Import multiple modules at once
sys.path Shows module search paths
Tiny Code
import math
radius = 3
area = math.pi * (radius 2)
print("Circle area:", area)
Why it Matters
Modules let you reuse existing solutions instead of reinventing the wheel. With imports, you
can access thousands of built-in and third-party libraries that extend Python’s power for math,
networking, data science, and more.
Try It Yourself
1. Import the math module and calculate the square root of 49.
2. Import the random module and generate a random integer between 1 and 100.
3. Use math.pi to compute the area of a circle with radius 10.
4. Print out the list of paths from sys.path and check where Python looks for modules.
Python comes with many built-in modules that provide ready-to-use functionality. Two of the
most commonly used are math (for mathematical operations) and random (for random number
generation).
87
Deep Dive
import math
print(math.sqrt(25)) # 5.0
print(math.pow(2, 3)) # 8.0
print(math.factorial(5)) # 120
print(math.pi) # 3.141592653589793
print(math.e) # 2.718281828459045
The random Module Used for randomness in numbers, selections, and shuffling.
Examples:
import random
88
Module Function Example Result
random random.random() float 0–1 e.g. 0.732
random random.randint(1,10) random int between 1 and 10
random random.choice(seq) random element one from list
random random.shuffle(seq) shuffle list reorders in place
Tiny Code
# math example
print("Cos(0):", math.cos(0))
# random example
colors = ["red", "green", "blue"]
random.shuffle(colors)
print("Shuffled colors:", colors)
Why it Matters
Built-in modules like math and random save you from writing code from scratch. They provide
reliable, optimized tools for tasks you’ll use frequently, from calculating areas to simulating
dice rolls.
Try It Yourself
Sometimes module names are long, or you want a shorter name for convenience. Python allows
you to alias a module (or part of it) using as. This doesn’t change the module—it just gives it
a nickname in your code.
89
Deep Dive
Basic Aliasing
import math as m
print(m.sqrt(16)) # 4.0
print(m.pi) # 3.14159...
print(fact(5)) # 120
Common Conventions Some libraries have standard aliases that are widely used in the Python
community:
• import numpy as np
• import pandas as pd
• import matplotlib.pyplot as plt
These conventions make code more readable because most developers recognize them instantly.
Why Use Aliases?
Statement Meaning
import module as alias give module a short name
from module import f as alias give function a short name
import numpy as np community standard alias
Tiny Code
90
import random as r
print(r.randint(1, 10))
Why it Matters
Aliasing helps keep code neat, prevents naming conflicts, and improves readability—especially
when using popular libraries with well-known abbreviations.
Try It Yourself
Instead of importing an entire module, you can import only the functions or variables you need.
This makes code shorter and sometimes clearer.
Deep Dive
Basic Syntax
print(sqrt(25)) # 5.0
print(pi) # 3.14159...
Here, we can use sqrt and pi directly without prefixing them with math..
Import with Aliases You can also alias imported items:
91
from math import factorial as fact
print(fact(5)) # 120
Importing Everything (Not Recommended) Using * imports all names from a module:
Statement Meaning
from math import sqrt Import only sqrt
from math import sqrt, pi Import multiple names
from math import factorial as f Import with alias
from math import * Import all (not recommended)
Tiny Code
Why it Matters
Importing specific functions makes code more concise and sometimes faster to read. It’s
especially useful when you’re using only a few tools from a module instead of the whole thing.
92
Try It Yourself
1. Import only sqrt and pow from math and use them to calculate sqrt(16) and 2^5.
2. Import randint from random and simulate rolling two dice.
3. Import pi from math and compute the circumference of a circle with radius 7.
4. Try using from math import *—then explain why this could cause confusion in larger
programs.
Python provides built-in functions like dir() and help() to let you explore modules, objects,
and their available functionality. These are extremely useful when you’re learning or working
with unfamiliar code.
Deep Dive
dir() → List Attributes dir(object) returns a list of all attributes (functions, variables,
classes) that an object has.
Example with a module:
import math
print(dir(math))
nums = [1, 2, 3]
print(dir(nums))
93
import random
help(random.randint)
randint(a, b)
Return a random integer N such that a <= N <= b.
Combining Both
Tiny Code
import math
Why it Matters
Instead of searching online every time, you can use dir() and help() inside Python itself.
This makes learning, debugging, and exploring modules much faster.
Try It Yourself
94
46. Creating Your Own Module
A module is just a Python file that you can reuse in other programs. By creating your own
module, you can organize code into separate files, making projects easier to maintain and
share.
Deep Dive
Step 1: Write a Module Any .py file can act as a module. Example — create a file called
mymath.py:
# mymath.py
def add(a, b):
return a + b
Step 2: Import the Module In another Python file (or interactive shell):
import mymath
print(mymath.add(2, 3)) # 5
print(mymath.multiply(4, 5)) # 20
print(add(10, 20)) # 30
Step 4: Module Location Python looks for modules in the current folder first, then in installed
libraries (sys.path). If your module is in the same directory, you can import it directly.
Special Variable: __name__ Inside every module, Python sets a special variable __name__.
This lets you write code that runs only when the file is executed, not when it’s imported.
95
# mymath.py
def add(a, b):
return a + b
if __name__ == "__main__":
print("Testing add:", add(2, 3))
Step Example
Create file mymath.py
Import whole module import mymath
Import specific function from mymath import add
Check module search path import sys; print(sys.path)
Run directly check if __name__ == "__main__": ...
Tiny Code
# File: greetings.py
def hello(name):
return f"Hello, {name}!"
# File: main.py
import greetings
print(greetings.hello("Alice"))
Why it Matters
Creating your own modules lets you structure larger projects, reuse code across different
scripts, and share your work with others. It’s the foundation for building Python packages and
libraries.
Try It Yourself
1. Create a file calculator.py with functions add, subtract, multiply, and divide.
2. Import it in a separate file and test each function.
3. Add a test block using if __name__ == "__main__": that runs some examples when
executed directly.
96
4. Create another module (e.g., greetings.py) and practice importing both in a single
script.
A package is a way to organize related modules into a directory. Unlike a single module (a .py
file), a package is a folder that contains an extra file called __init__.py. This tells Python to
treat the folder as a package.
Deep Dive
Basic Structure
mypackage/
__init__.py
math_utils.py
string_utils.py
• __init__.py → can be empty, or it can define what gets imported when the package is
used.
• math_utils.py and string_utils.py → normal Python modules.
import mypackage.math_utils
print(mypackage.math_utils.add(2, 3))
97
# mypackage/__init__.py
from .math_utils import add
from .string_utils import reverse
mypackage/
__init__.py
utils/
__init__.py
file_utils.py
Access with:
import mypackage.utils.file_utils
Term Meaning
Module Single .py file
Package Directory with __init__.py + modules
Sub-package Package inside another package
Import import mypackage.module
Simplify import Define exports in __init__.py
Tiny Code
mypackage/
__init__.py
greetings.py
98
# greetings.py
def hello(name):
return f"Hello, {name}!"
# main.py
from mypackage import greetings
print(greetings.hello("Alice"))
Why it Matters
Packages make it easy to organize large projects into smaller, logical parts. They allow you to
group related modules together, keep code clean, and make it reusable for others.
Try It Yourself
1. Create a folder shapes/ with __init__.py and a module circle.py that has area(r).
2. Import circle in another file and test the function.
3. Add another module square.py with area(s) and import both.
4. Modify __init__.py so you can do from shapes import area for both circle and
square.
While Python’s standard library is powerful, you’ll often need third-party packages. Python
uses pip (Python Package Installer) to download and manage these packages from the Python
Package Index (PyPI).
Deep Dive
Check if pip is Installed Most modern Python versions include it by default. You can check
with:
pip --version
Installing a Package
99
This downloads and installs the popular requests library for making HTTP requests.
Using the Installed Package
import requests
response = requests.get("https://api.github.com")
print(response.status_code) # 200
Upgrading a Package
Uninstalling a Package
pip list
Requirements File You can save dependencies in a file (requirements.txt) so others can
install them easily:
requests==2.31.0
numpy>=1.25
Command Purpose
pip install package Install a package
100
Command Purpose
pip install --upgrade package Update a package
pip uninstall package Remove a package
pip list Show installed packages
pip freeze > requirements.txt Save current dependencies
pip install -r requirements.txt Install from requirements file
Tiny Code
import numpy as np
Why it Matters
pip opens the door to Python’s massive ecosystem. Whether you need data analysis (pandas),
machine learning (scikit-learn), or web frameworks (Flask, Django), you can install them
in seconds and start building.
Try It Yourself
A virtual environment is a self-contained directory that holds a specific Python version and its
installed packages. It allows you to isolate dependencies for different projects so they don’t
conflict with each other.
101
Deep Dive
This creates a folder myenv/ with its own Python interpreter and libraries.
Activating the Environment
• On Windows:
myenv\Scripts\activate
• On Mac/Linux:
source myenv/bin/activate
You’ll see (myenv) appear in your terminal prompt, showing it’s active.
Installing Packages Inside Once activated, use pip normally—it only affects this environment:
deactivate
Command Purpose
python -m venv myenv Create a virtual environment
source myenv/bin/activate Activate (Mac/Linux)
102
Command Purpose
myenv\Scripts\activate Activate (Windows)
pip install package Install inside environment
deactivate Exit environment
Tiny Code
Why it Matters
Virtual environments are essential for professional Python development. They ensure each
project has the right dependencies and prevent “it works on my machine” problems.
Try It Yourself
Beyond the Python standard library, the community has built thousands of powerful third-party
packages available through PyPI (Python Package Index). These extend Python’s capabilities
for web development, data analysis, machine learning, automation, and more.
103
Deep Dive
Web Development
@app.route("/")
def home():
return "Hello, Flask!"
import pandas as pd
104
• schedule → lightweight task scheduler.
Tiny Code
import requests
response = requests.get("https://api.github.com")
print("Status:", response.status_code)
Why it Matters
Third-party packages are what make Python one of the most popular languages today. Whether
you want to build websites, analyze data, or train AI models, there’s a package ready to help
you.
Try It Yourself
1. Use pip install requests and fetch data from any website.
2. Install pandas and create a small table of data.
3. Install matplotlib and draw a simple line chart.
4. Explore PyPI (https://pypi.org) and find a package that interests you.
105
Chapter 6. File Handling
Working with files is a core part of programming. Python’s built-in open() function lets you
read from and write to files easily.
Deep Dive
Basic Syntax
Common modes:
106
Error Handling If the file doesn’t exist in "r" mode, Python raises an error:
Tiny Code
# Write a file
f = open("hello.txt", "w")
f.write("Hello, world!")
f.close()
Why it Matters
Files let you store information permanently. Whether saving logs, configurations, or datasets,
file handling is essential for almost every real-world Python project.
Try It Yourself
1. Create a file notes.txt and write three lines of text into it.
2. Reopen the file in "r" mode and print the contents.
3. Open the same file in "a" mode and add another line.
4. Try opening a non-existent file in "r" mode and see the error.
107
52. Reading Files
Once you open a file in read mode, you can extract its contents in different ways depending on
your needs: the whole file, line by line, or into a list.
Deep Dive
f = open("notes.txt", "r")
content = f.read()
print(content)
f.close()
f = open("notes.txt", "r")
line1 = f.readline()
line2 = f.readline()
print(line1, line2)
f.close()
• Each call to readline() gets the next line (including the \n).
f = open("notes.txt", "r")
lines = f.readlines()
print(lines)
f.close()
f = open("notes.txt", "r")
for line in f:
print(line.strip())
f.close()
108
Quick Summary Table
Tiny Code
Why it Matters
Reading files is fundamental to processing data. Whether you’re analyzing logs, reading
configurations, or loading datasets, understanding the different read methods helps you handle
small and large files efficiently.
Try It Yourself
Python lets you write text to files using the write() and writelines() methods. This is
useful for saving logs, results, or any output that needs to be stored permanently.
109
Deep Dive
Write Text with write() Opening a file in "w" mode will overwrite it if it already exists, or
create it if it doesn’t.
f = open("output.txt", "w")
f.write("Hello, world!\n")
f.write("This is a new line.\n")
f.close()
Append Mode ("a") To keep existing content and add to the end:
f = open("output.txt", "a")
f.write("Adding more text here.\n")
f.close()
f = open("multi.txt", "w")
f.writelines(lines)
f.close()
� Note: writelines() does not add newlines automatically—you must include \n yourself.
Best Practice with with Automatically closes the file after writing:
110
Tiny Code
Why it Matters
Being able to write files is crucial for persisting data beyond program execution. Logs, reports,
exported data, and notes all rely on writing to files.
Try It Yourself
1. Create a file journal.txt and write three lines about your day.
2. Open the file again in "a" mode and add two more lines.
3. Use writelines() to add a list of tasks into tasks.txt.
4. Reopen and read back the contents to confirm everything was saved.
When opening files in Python with open(), the mode determines how the file is accessed—read,
write, append, or binary. Understanding modes is essential to avoid overwriting or corrupting
files.
Deep Dive
Binary Modes Add "b" to handle non-text files (images, audio, executables).
111
• "rb" → read binary.
• "wb" → write binary.
• "ab" → append binary.
# Reading an image
with open("photo.jpg", "rb") as f:
data = f.read()
# Writing binary
with open("copy.jpg", "wb") as f:
f.write(data)
Tiny Code
# Write + read
with open("sample.txt", "w+") as f:
f.write("Hello!\n")
f.seek(0)
print(f.read())
112
Why it Matters
Choosing the right mode ensures you don’t lose data accidentally (like "w" erasing files) and
allows you to correctly handle binary files like images or PDFs.
Try It Yourself
1. Open a file in "w" mode and write two lines. Reopen it in "r" mode and confirm old
content was overwritten.
2. Open the same file in "a" mode and add another line.
3. Try using "x" mode to create a new file. Run it twice and observe the error on the second
run.
4. Copy an image using "rb" and "wb".
When you open a file in Python, the system allocates resources to manage it. To free these
resources and ensure all data is written properly, you must close the file once you’re done.
Deep Dive
f = open("notes.txt", "w")
f.write("Hello, file!")
f.close()
f = open("notes.txt", "r")
print(f.closed) # False
f.close()
print(f.closed) # True
Best Practice: with Statement Instead of manually calling close(), use with. It automatically
closes the file, even if an error occurs.
113
with open("notes.txt", "r") as f:
content = f.read()
print(f.closed) # True
Flushing Without Closing If you want to save changes but keep the file open:
f = open("data.txt", "w")
f.write("Line 1\n")
f.flush() # forces write to disk
# file still open
f.close()
Method Behavior
f.close() Manually closes the file
f.closed Check if file is closed
f.flush() Force save data without closing
with open() Automatically closes after block
Tiny Code
Why it Matters
Closing files ensures data safety and efficient resource usage. Forgetting to close files can lead
to bugs, data loss, or locked files. The with statement makes it almost impossible to forget.
114
Try It Yourself
1. Open a file in write mode, write some text, and check f.closed before and after calling
close().
2. Use with open() to write two lines and verify that the file is closed outside the block.
3. Experiment with f.flush()—write text, flush, then write more before closing.
4. Try opening many files in a loop without closing them, then observe system warnings/er-
rors.
The with statement in Python provides a clean and safe way to work with files. It automatically
takes care of opening and closing the file, even if errors occur while processing.
Deep Dive
Basic Usage
Multiple Files with One with You can work with multiple files in a single with statement:
115
with open("input.txt", "r") as infile, open("copy.txt", "w") as outfile:
for line in infile:
outfile.write(line)
Custom Context Managers The with statement isn’t just for files—it works with anything that
supports the context manager protocol (__enter__ and __exit__).
Example:
class MyResource:
def __enter__(self):
print("Resource acquired")
return self
def __exit__(self, exc_type, exc_value, traceback):
print("Resource released")
with MyResource():
print("Using resource")
Feature Example
Auto-close file with open("f.txt") as f:
Write file with open("f.txt","w") as f: f.write("x")
Multiple files with open("a.txt") as a, open("b.txt") as b:
Custom manager Define __enter__, __exit__
Tiny Code
Why it Matters
The with statement is the best practice for file handling in Python. It makes code safer, shorter,
and more reliable by guaranteeing cleanup.
116
Try It Yourself
1. Use with open("log.txt", "w") to write three lines. Confirm the file is closed after-
wards.
2. Copy the contents of one file into another using a with block.
3. Experiment by raising an error inside a with block—notice the file is still closed.
4. Create a simple class with __enter__ and __exit__ to practice writing your own context
manager.
CSV (Comma-Separated Values) files are widely used for storing tabular data like spreadsheets
or databases. Python’s built-in csv module makes it easy to read and write CSV files.
Deep Dive
import csv
import csv
rows = [
["Name", "Age"],
["Alice", 25],
["Bob", 30]
]
117
• writerow() → writes a single row.
• writerows() → writes multiple rows.
• newline="" avoids blank lines on Windows.
Using Dictionaries with CSV Instead of working with lists, you can use DictReader and
DictWriter.
import csv
# Writing
with open("people.csv", "w", newline="") as f:
fieldnames = ["Name", "Age"]
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerow({"Name": "Charlie", "Age": 35})
# Reading
with open("people.csv", "r") as f:
reader = csv.DictReader(f)
for row in reader:
print(row["Name"], row["Age"])
Class/Function Purpose
csv.reader Reads CSV into lists
csv.writer Writes CSV from lists
csv.DictReader Reads CSV into dictionaries
csv.DictWriter Writes CSV from dictionaries
Tiny Code
import csv
118
with open("scores.csv", "r") as f:
reader = csv.reader(f)
for row in reader:
print(row)
Why it Matters
CSV is the most common format for sharing data between systems. By mastering the csv
module, you can process spreadsheets, export reports, and integrate with databases or analytics
tools.
Try It Yourself
JSON (JavaScript Object Notation) is a lightweight data format often used for APIs, configs,
and data exchange. Python has a built-in json module that makes it easy to read and write
JSON files.
Deep Dive
import json
import json
data = {
"name": "Alice",
"age": 25,
"languages": ["Python", "JavaScript"]
}
119
with open("data.json", "w") as f:
json.dump(data, f)
print(loaded["name"]) # Alice
print(loaded["languages"]) # ['Python', 'JavaScript']
s = json.dumps(data)
print(s) # '{"name": "Alice", "age": 25, ...}'
obj = json.loads(s)
print(obj["age"]) # 25
print(json.dumps(data, indent=4))
Function Purpose
json.dump(obj,f) Write JSON to a file
json.load(f) Read JSON from a file
json.dumps(obj) Convert object to JSON string
json.loads(str) Convert JSON string to Python object
Tiny Code
120
import json
Why it Matters
JSON is the universal format for modern applications—from web APIs to configuration files.
By mastering Python’s json module, you can easily communicate with APIs, save structured
data, and exchange information with other systems.
Try It Yourself
1. Create a dictionary with your name, age, and hobbies, then save it to me.json.
2. Reopen me.json and print the hobbies.
3. Use json.dumps() to print the same dictionary as a formatted JSON string.
4. Convert a JSON string back into a Python dictionary using json.loads().
When working with files, many things can go wrong: the file might not exist, permissions might
be missing, or the disk might be full. Python uses exceptions to handle these errors safely.
Deep Dive
121
Handling File Exceptions
try:
f = open("missing.txt", "r")
content = f.read()
f.close()
except FileNotFoundError:
print("The file does not exist.")
try:
f = open("/protected/data.txt", "r")
except (FileNotFoundError, PermissionError) as e:
print("Error:", e)
try:
f = open("data.txt", "r")
print(f.read())
finally:
f.close() # ensures file closes even on error
Safer with with The with statement avoids many of these issues automatically, but exceptions
can still happen when opening:
try:
with open("notes.txt", "r") as f:
print(f.read())
except FileNotFoundError:
print("File not found!")
Exception Cause
FileNotFoundError File does not exist
PermissionError No permission to access file
IsADirectoryError Tried to open a directory as a file
IOError / OSError General input/output failure
122
Tiny Code
filename = "example.txt"
try:
with open(filename, "r") as f:
print(f.read())
except FileNotFoundError:
print(f"Error: {filename} was not found.")
Why it Matters
Errors in file handling are inevitable. Exception handling makes your programs robust, user-
friendly, and prevents crashes when dealing with unpredictable files and systems.
Try It Yourself
1. Try opening a file that doesn’t exist, catch the FileNotFoundError, and print a custom
message.
2. Write code that catches both FileNotFoundError and PermissionError.
3. Use finally to always print "Done" after attempting to open a file.
4. Combine with open() and try...except to safely read a file only if it exists.
Working with files often means dealing with paths and directories. Python provides two main
tools for this: the older os module and the modern pathlib module.
Deep Dive
import os
print(os.getcwd()) # shows current directory
With pathlib:
123
from pathlib import Path
print(Path.cwd())
Changing Directory
os.chdir("/tmp")
With pathlib:
p = Path(".")
for file in p.iterdir():
print(file)
With pathlib:
Path("folder") / "file.txt"
os.path.exists("notes.txt") # True/False
With pathlib:
p = Path("notes.txt")
print(p.exists())
print(p.is_file())
print(p.is_dir())
Creating Directories
124
os.mkdir("newfolder")
With parents:
Path("a/b/c").mkdir(parents=True, exist_ok=True)
With pathlib:
Path("file.txt").unlink()
Tiny Code
p = Path("demo_folder")
p.mkdir(exist_ok=True)
file = p / "hello.txt"
file.write_text("Hello, pathlib!")
print(file.read_text())
125
Why it Matters
Paths and directories are essential for any project involving files. pathlib provides a modern,
object-oriented approach, while os ensures backward compatibility with older code. Knowing
both makes you flexible.
Try It Yourself
Deep Dive
Defining a Class
class Person:
pass
p1 = Person()
print(type(p1)) # <class '__main__.Person'>
126
class Person:
def __init__(self, name, age):
self.name = name # attribute
self.age = age
p1 = Person("Alice", 25)
print(p1.name, p1.age) # Alice 25
Adding Methods
class Person:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, my name is {self.name}."
p1 = Person("Bob")
print(p1.greet()) # Hello, my name is Bob.
Tiny Code
class Dog:
def __init__(self, name, breed):
self.name = name
127
self.breed = breed
def bark(self):
return f"{self.name} says Woof!"
d1 = Dog("Max", "Labrador")
print(d1.bark())
Why it Matters
Classes and objects are the foundation of OOP. They let you model real-world things (like cars,
users, or bank accounts) in code, organize functionality, and build scalable applications.
Try It Yourself
In Python classes, attributes are variables that belong to objects, and methods are functions
that belong to objects. Together, they define what an object has (data) and what it does
(behavior).
Deep Dive
class Car:
def __init__(self, brand, year):
self.brand = brand
self.year = year
c1 = Car("Toyota", 2020)
print(c1.brand) # Toyota
print(c1.year) # 2020
128
Here, brand and year are attributes of the Car object.
Instance Methods (Object Behavior) Methods define actions an object can perform.
class Car:
def __init__(self, brand, year):
self.brand = brand
self.year = year
def drive(self):
return f"{self.brand} is driving."
c1 = Car("Honda", 2019)
print(c1.drive()) # Honda is driving.
c1.year = 2022
print(c1.year) # 2022
c1.color = "red"
print(c1.color) # red
class Dog:
species = "Canis lupus familiaris" # class attribute
def __init__(self, name):
self.name = name # instance attribute
d1 = Dog("Buddy")
d2 = Dog("Charlie")
print(d1.species, d2.species) # same for all
print(d1.name, d2.name) # unique per dog
129
Quick Summary Table
Tiny Code
class Student:
school = "Python Academy" # class attribute
def introduce(self):
return f"I am {self.name}, grade {self.grade}."
s1 = Student("Alice", "A")
s2 = Student("Bob", "B")
print(s1.introduce())
print(s2.introduce())
print("School:", s1.school)
Why it Matters
Attributes and methods are the building blocks of object-oriented programming. Attributes
give objects state, while methods give them behavior. Together, they let you model real-world
entities in code.
Try It Yourself
130
63. __init__ Constructor
In Python, the __init__ method is a special method that runs automatically when you create
a new object. It’s often called the constructor because it initializes (sets up) the object’s
attributes.
Deep Dive
Basic Example
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p1 = Person("Alice", 25)
print(p1.name, p1.age) # Alice 25
class Person:
def __init__(self, name="Unknown", age=0):
self.name = name
self.age = age
p1 = Person()
print(p1.name, p1.age) # Unknown 0
Constructor with Logic You can add checks or calculations during initialization:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
self.area = width * height # auto-calculate
r = Rectangle(4, 5)
print(r.area) # 20
131
Multiple Objects, Independent Attributes Each object gets its own copy of instance attributes:
p1 = Person("Alice", 25)
p2 = Person("Bob", 30)
print(p1.name) # Alice
print(p2.name) # Bob
Tiny Code
class Dog:
def __init__(self, name, breed="Unknown"):
self.name = name
self.breed = breed
d1 = Dog("Max", "Beagle")
d2 = Dog("Charlie")
print(d1.name, d1.breed)
print(d2.name, d2.breed)
Why it Matters
The __init__ constructor ensures every object starts in a well-defined state. Without it, you’d
have to manually assign attributes after creating objects, which is error-prone and messy.
Try It Yourself
1. Create a Car class with attributes brand, model, and year set in __init__.
2. Add a method info() that prints "Brand Model (Year)".
132
3. Give year a default value if not provided.
4. Create two Car objects—one with all values, one with just brand and model—and call
info() on both.
In Python classes, variables can belong either to a specific object (instance variables) or to
the class itself (class variables). Knowing the difference is key to writing predictable, reusable
code.
Deep Dive
Instance Variables
class Dog:
def __init__(self, name):
self.name = name # instance variable
d1 = Dog("Buddy")
d2 = Dog("Charlie")
print(d1.name) # Buddy
print(d2.name) # Charlie
class Dog:
species = "Canis lupus familiaris" # class variable
d1 = Dog("Buddy")
d2 = Dog("Charlie")
133
print(d1.species) # Canis lupus familiaris
print(d2.species) # Canis lupus familiaris
Dog.species = "Dog"
print(d1.species, d2.species) # Dog Dog
Overriding Class Variables per Instance You can assign a new value to a class variable on a
specific object, but then it becomes an instance variable for that object only:
Tiny Code
class Student:
school = "Python Academy" # class variable
s1 = Student("Alice")
s2 = Student("Bob")
134
Why it Matters
Try It Yourself
Inheritance allows one class to take on the attributes and methods of another. This promotes
code reuse and models real-world relationships (e.g., a Dog is an Animal).
Deep Dive
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound."
a = Animal("Generic")
print(a.speak()) # Generic makes a sound.
d = Dog("Buddy")
print(d.speak()) # Buddy makes a sound. (inherited)
print(d.bark()) # Buddy says Woof! (own method)
135
The super() Function super() lets the child class call methods from the parent class.
class Animal:
def __init__(self, name):
self.name = name
class Cat(Animal):
def __init__(self, name, color):
super().__init__(name) # call parent constructor
self.color = color
c = Cat("Luna", "Gray")
print(c.name, c.color) # Luna Gray
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof!"
print(Dog().speak()) # Woof!
Inheritance Hierarchy
136
Tiny Code
class Vehicle:
def __init__(self, brand):
self.brand = brand
def drive(self):
return f"{self.brand} is moving."
class Car(Vehicle):
def drive(self):
return f"{self.brand} is driving on the road."
v = Vehicle("Generic Vehicle")
c = Car("Toyota")
print(v.drive())
print(c.drive())
Why it Matters
Inheritance reduces duplication and makes code more organized. By building hierarchies, you
can model relationships between classes naturally, reusing and extending existing functionality.
Try It Yourself
Method overriding happens when a child class defines a method with the same name as one in
its parent class. The child’s version replaces (overrides) the parent’s when called on a child
object.
137
Deep Dive
Basic Example
class Animal:
def speak(self):
return "Some generic sound"
class Dog(Animal):
def speak(self): # overrides parent method
return "Woof!"
a = Animal()
d = Dog()
Why Override?
Using super() with Overrides You can call the parent’s version inside the override:
class Vehicle:
def drive(self):
return "The vehicle is moving."
class Car(Vehicle):
def drive(self):
parent_drive = super().drive()
return parent_drive + " Specifically, the car is driving."
c = Car()
print(c.drive())
Partial Overrides You don’t always have to replace the entire method—you can extend it:
class Logger:
def log(self, message):
print("Log:", message)
138
class TimestampLogger(Logger):
def log(self, message):
import datetime
time = datetime.datetime.now()
super().log(f"{time} - {message}")
Tiny Code
class Employee:
def work(self):
return "Employee is working."
class Manager(Employee):
def work(self):
return "Manager is planning and managing."
e = Employee()
m = Manager()
Why it Matters
Method overriding lets subclasses adapt behavior without rewriting everything from scratch.
It’s a cornerstone of polymorphism, where different classes can define the same method name
but act differently.
139
Try It Yourself
1. Create a base class Animal with sound() that returns "Unknown sound".
2. Make Dog and Cat subclasses that override sound() with "Woof" and "Meow".
3. Use a loop to call sound() on both objects and see polymorphism in action.
4. Extend the base method in one subclass using super() to add extra behavior.
Python allows a class to inherit from more than one parent class. This is called multiple
inheritance. It can be powerful but must be used carefully to avoid confusion.
Deep Dive
Basic Example
class Flyer:
def fly(self):
return "I can fly!"
class Swimmer:
def swim(self):
return "I can swim!"
d = Duck()
print(d.fly()) # I can fly!
print(d.swim()) # I can swim!
class A:
def hello(self):
return "Hello from A"
class B(A):
140
def hello(self):
return "Hello from B"
class C(A):
def hello(self):
return "Hello from C"
d = D()
print(d.hello()) # Hello from B
print(D.mro()) # [D, B, C, A, object]
Using super() with Multiple Inheritance super() respects the MRO, allowing cooperative
behavior:
class A:
def action(self):
print("A action")
class B(A):
def action(self):
super().action()
print("B action")
class C(A):
def action(self):
super().action()
print("C action")
d = D()
d.action()
Output:
141
A action
C action
B action
D action
Concept Meaning
Multiple inheritance Class inherits from more than one parent
MRO Defines search order for methods/attributes
Diamond problem Ambiguity when same method exists in parents
super() in MRO Ensures cooperative method calls
Tiny Code
class Writer:
def write(self):
return "Writing..."
class Reader:
def read(self):
return "Reading..."
a = Author()
print(a.write())
print(a.read())
Why it Matters
Multiple inheritance allows you to combine behaviors from different classes, making code
flexible and modular. But without understanding MRO, it can introduce bugs and unexpected
results.
142
Try It Yourself
Encapsulation is the principle of restricting direct access to some parts of an object, protecting
its internal state. In Python, this is done through naming conventions rather than strict
enforcement.
Deep Dive
Public Members
class Person:
def __init__(self, name):
self.name = name # public attribute
p = Person("Alice")
print(p.name) # Alice
class Person:
def __init__(self, name):
self._secret = "hidden"
p = Person("Alice")
print(p._secret) # possible, but discouraged
143
• Indicated with double underscores.
• Name-mangled to prevent accidental access.
class BankAccount:
def __init__(self, balance):
self.__balance = balance # private
def get_balance(self):
return self.__balance
acc = BankAccount(100)
acc.deposit(50)
print(acc.get_balance()) # 150
print(acc.__balance) # AttributeError
print(acc._BankAccount__balance) # works (name-mangled)
Why Encapsulation?
Tiny Code
144
class Student:
def __init__(self, name, grade):
self.name = name # public
self._grade = grade # protected
self.__id = 12345 # private
def get_id(self):
return self.__id
s = Student("Bob", "A")
print(s.name) # Public
print(s._grade) # Accessible but discouraged
print(s.get_id()) # Safe access
Why it Matters
Encapsulation protects the integrity of your objects. By controlling access, you reduce bugs
and make your code safer and more maintainable.
Try It Yourself
Python classes can define special methods (also called dunder methods, because they have
double underscores). These let objects behave like built-in types and integrate smoothly with
Python features.
Deep Dive
145
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
p = Person("Alice", 25)
print(p) # Alice, 25 years old
class Person:
def __repr__(self):
return f"Person(name='{self.name}', age={self.age})"
class Team:
def __init__(self, members):
self.members = members
def __len__(self):
return len(self.members)
t = Team(["Alice", "Bob"])
print(len(t)) # 2
class Notebook:
def __init__(self):
self.notes = {}
def __getitem__(self, key):
return self.notes[key]
def __setitem__(self, key, value):
self.notes[key] = value
n = Notebook()
n["day1"] = "Learn Python"
print(n["day1"]) # Learn Python
146
• __eq__ → equality (==)
• __lt__ → less than (<)
• __add__ → addition (+)
• __call__ → make object callable like a function
• __iter__ → make object iterable in for loops
Tiny Code
class Counter:
def __init__(self, count=0):
self.count = count
def __str__(self):
return f"Counter({self.count})"
c1 = Counter(3)
c2 = Counter(7)
print(c1) # Counter(3)
print(c1 + c2) # Counter(10)
Why it Matters
Special methods let you design objects that feel natural to use, just like built-in types. This
makes your classes more powerful, expressive, and Pythonic.
147
Try It Yourself
1. Create a Book class with title and author, and override __str__ to print "Title by
Author".
2. Add __len__ to return the length of the title.
3. Implement __eq__ to compare two books by title and author.
4. Implement __add__ so that adding two books returns a string joining both titles.
In Python, not all methods need to work with a specific object. Sometimes they belong to the
class itself. Python provides class methods and static methods for these cases.
Deep Dive
class Person:
def greet(self):
return "Hello!"
class Person:
species = "Homo sapiens"
@classmethod
def get_species(cls):
return cls.species
148
• A regular function inside a class for logical grouping.
• Declared with @staticmethod.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
print(MathUtils.add(5, 7)) # 12
Tiny Code
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5/9)
@staticmethod
def is_freezing(temp_c):
return temp_c <= 0
t = Temperature.from_fahrenheit(32)
print(t.celsius) # 0.0
print(Temperature.is_freezing(-5)) # True
149
Why it Matters
Static and class methods give you more flexibility in structuring code. They help keep related
functions together inside classes, even if they don’t act on specific objects.
Try It Yourself
1. Create a Circle class with a class variable pi = 3.14. Add a @classmethod get_pi()
that returns it.
2. Add a @staticmethod area(radius) that computes circle area using pi.
3. Create a circle and check both methods.
4. Try calling them on both the class and an instance.
An exception is an error that happens during program execution, interrupting the normal flow.
Unlike syntax errors (which stop code before running), exceptions occur at runtime and can be
handled so the program doesn’t crash.
Deep Dive
print(10 / 0) # ZeroDivisionError
numbers = [1, 2, 3]
print(numbers[5]) # IndexError
int("hello") # ValueError
open("nofile.txt") # FileNotFoundError
150
– KeyError → missing dictionary key.
– OSError → file system-related errors.
Tiny Code
try:
num = int("abc") # invalid conversion
except ValueError:
print("Oops! That was not a valid number.")
Why it Matters
Exceptions are unavoidable in real-world programs. By understanding them, you can write
code that fails gracefully instead of crashing unexpectedly.
Try It Yourself
151
72. Common Exceptions (ValueError, TypeError, etc.)
Python has many built-in exceptions that you will encounter often. Knowing them helps you
quickly identify problems and handle them gracefully.
Deep Dive
ValueError Occurs when a function gets the right type of input but an inappropriate value.
int("hello") # ValueError
TypeError Occurs when an operation or function is applied to an object of the wrong type.
IndexError Happens when you try to access an index outside the valid range of a list.
nums = [1, 2, 3]
print(nums[5]) # IndexError
KeyError Raised when trying to access a dictionary key that doesn’t exist.
FileNotFoundError Occurs when you try to open a file that doesn’t exist.
open("missing.txt") # FileNotFoundError
10 / 0 # ZeroDivisionError
152
Exception Example Trigger
IndexError [1,2,3][10]
KeyError {"a":1}["b"]
FileNotFoundError open("nofile.txt")
ZeroDivisionError 1 / 0
Tiny Code
try:
nums = [1, 2, 3]
print(nums[10])
except IndexError:
print("Oops! That index doesn't exist.")
Why it Matters
These exceptions are among the most frequent in Python. Understanding them helps you
debug faster and design safer programs by predicting possible errors.
Try It Yourself
Python uses try and except to handle exceptions gracefully. Instead of crashing, the program
jumps to the except block when an error occurs.
Deep Dive
Basic Structure
153
try:
# code that may cause an error
x = int("abc")
except ValueError:
print("That was not a number!")
Catching Different Exceptions You can handle multiple specific errors separately:
try:
result = 10 / 0
except ZeroDivisionError:
print("You can't divide by zero.")
except ValueError:
print("Invalid value.")
try:
f = open("nofile.txt")
except Exception as e:
print("Error occurred:", e)
try:
print("Before error")
x = 5 / 0
print("This won't run")
except ZeroDivisionError:
print("Handled division by zero")
154
Keyword Purpose
try Wraps code that may cause an error
except Defines how to handle specific exceptions
as e Captures the exception object
Tiny Code
try:
num = int("42a")
print("Converted:", num)
except ValueError as e:
print("Error:", e)
Why it Matters
try/except is the foundation of error handling in Python. It lets you recover from errors, give
helpful messages, and keep your program running.
Try It Yourself
Sometimes, different types of errors can occur in the same block of code. Python allows you to
handle multiple exceptions separately or together.
Deep Dive
Separate Except Blocks You can write different handlers for each type of exception:
155
try:
x = int("abc") # may cause ValueError
y = 10 / 0 # may cause ZeroDivisionError
except ValueError:
print("Invalid conversion to int.")
except ZeroDivisionError:
print("Cannot divide by zero.")
Catching Multiple Exceptions in One Block You can group exceptions in a tuple:
try:
data = [1, 2, 3]
print(data[5]) # IndexError
except (ValueError, IndexError) as e:
print("Caught an error:", e)
Generic Catch-All The Exception base class catches everything derived from it:
try:
result = 10 / 0
except Exception as e:
print("Something went wrong:", e)
try:
10 / 0
except Exception:
print("General error") # this will run
except ZeroDivisionError:
print("Specific error") # never reached
156
Style Example Use Case
General Exception catch-all except Exception as Debugging, fallback handling
e:
Tiny Code
try:
num = int("xyz")
result = 10 / 0
except ValueError:
print("Conversion failed.")
except ZeroDivisionError:
print("Math error: division by zero.")
Why it Matters
Most real-world code must guard against different failure modes. Being able to catch multiple
exceptions lets you handle each case correctly without stopping the whole program.
Try It Yourself
In Python, you can use an else block with try/except. The else block runs only if no
exception was raised in the try block.
Deep Dive
Basic Structure
157
try:
x = int("42") # no error here
except ValueError:
print("Conversion failed.")
else:
print("Conversion successful:", x)
• Keeps your try block focused only on code that might fail.
• Puts the “safe” code in else, separating it clearly.
Example:
try:
f = open("data.txt")
except FileNotFoundError:
print("File not found.")
else:
print("File opened successfully.")
f.close()
try:
num = int("100")
except ValueError:
print("Invalid number.")
else:
print("Parsed successfully:", num)
158
Tiny Code
try:
result = 10 / 2
except ZeroDivisionError:
print("Division failed.")
else:
print("Division successful:", result)
Why it Matters
Using else makes exception handling cleaner: risky code in try, error handling in except,
and safe follow-up code in else. This improves readability and reduces mistakes.
Try It Yourself
1. Write code that reads a number from a string with int(). If it fails, handle ValueError.
If it succeeds, print "Valid number" in else.
2. Try dividing two numbers, catching ZeroDivisionError, and use else to print the result
if successful.
3. Open an existing file in try, handle FileNotFoundError, and confirm success in else.
In Python, the finally block is used with try/except to guarantee that certain code always
runs — no matter what happens. This is useful for cleanup tasks like closing files or releasing
resources.
Deep Dive
Basic Structure
try:
x = 10 / 2
except ZeroDivisionError:
print("Division failed.")
finally:
print("This always runs.")
159
• If no error: finally still runs.
• If an error occurs and is caught: finally still runs.
• If an error occurs and is not caught: finally still runs before the program crashes.
try:
num = int("42")
except ValueError:
print("Invalid number")
else:
print("Conversion successful:", num)
finally:
print("Execution finished")
1. try block
2. except (if error) OR else (if no error)
3. finally (always)
try:
f = open("data.txt", "r")
content = f.read()
except FileNotFoundError:
print("File not found.")
finally:
print("Closing file...")
try:
f.close()
except:
pass
160
Tiny Code
try:
print("Opening file...")
f = open("missing.txt")
except FileNotFoundError:
print("Error: File not found.")
finally:
print("Cleanup done.")
Why it Matters
The finally block ensures important cleanup (like closing files, saving data, disconnecting
from databases) always happens — even if the program crashes in the middle.
Try It Yourself
1. Write code that divides two numbers with try/except, then add a finally block to
print "End of operation".
2. Try opening a file in try, handle FileNotFoundError, and in finally print "Closing
resources".
3. Combine try, except, else, and finally in one program and observe the execution
order.
Sometimes, instead of waiting for Python to throw an error, you may want to raise an exception
yourself when something unexpected happens. This is done with the raise keyword.
Deep Dive
Basic Usage
161
print(divide(10, 2)) # 5.0
print(divide(5, 0)) # Raises ZeroDivisionError
age = -1
if age < 0:
raise ValueError("Age cannot be negative")
name = ""
if not name:
raise Exception("Name must not be empty")
Re-raising Exceptions Sometimes you catch an error but still want to pass it upward:
try:
x = int("abc")
except ValueError as e:
print("Caught an error:", e)
raise # re-raises the same exception
Tiny Code
162
def check_age(age):
if age < 18:
raise ValueError("Must be at least 18 years old.")
return "Access granted."
Why it Matters
Raising exceptions gives you control. Instead of letting bad data silently continue, you can
stop execution, show a meaningful error, and prevent bigger problems later.
Try It Yourself
In addition to Python’s built-in exceptions, you can define your own custom exceptions to
make error handling more meaningful in your programs.
Deep Dive
Defining a Custom Exception A custom exception is just a class that inherits from Python’s
built-in Exception class.
class NegativeNumberError(Exception):
"""Raised when a number is negative."""
pass
163
def square_root(x):
if x < 0:
raise NegativeNumberError("Cannot take square root of negative number")
return x 0.5
print(square_root(9)) # 3.0
print(square_root(-4)) # Raises NegativeNumberError
Adding Extra Functionality You can extend custom exceptions with attributes.
class BalanceError(Exception):
def __init__(self, balance, message="Insufficient funds"):
self.balance = balance
self.message = message
super().__init__(f"{message}. Balance: {balance}")
try:
square_root(-1)
except NegativeNumberError as e:
print("Custom error caught:", e)
Step Example
Define custom error class MyError(Exception): ...
Raise it raise MyError("Something happened")
164
Step Example
Catch it except MyError as e:
Tiny Code
class AgeError(Exception):
pass
def register(age):
if age < 18:
raise AgeError("Must be 18 or older to register")
return "Registered!"
try:
print(register(16))
except AgeError as e:
print("Registration failed:", e)
Why it Matters
Custom exceptions make your programs more self-explanatory and professional. Instead of
generic errors, you provide meaningful messages tailored to your application’s domain.
Try It Yourself
An assertion is a quick way to test if a condition in your program is true. If the condition is
false, Python raises an AssertionError. Assertions are often used for debugging and catching
mistakes early.
165
Deep Dive
Basic Usage
x = 5
assert x > 0 # passes, nothing happens
assert x < 0 # fails, raises AssertionError
age = -1
assert age >= 0, "Age cannot be negative"
• Assertions can be disabled when running Python with the -O (optimize) flag.
• Example: python -O program.py → all assert statements are skipped.
Practical Example
Syntax Behavior
assert condition Raises AssertionError if condition false
166
Syntax Behavior
assert condition, msg Raises with custom message
Disabled with -O Skips all asserts
Tiny Code
score = 95
assert 0 <= score <= 100, "Score must be between 0 and 100"
print("Score is valid!")
Why it Matters
Assertions help you detect logic errors early. They make your intentions clear in code and act
as built-in sanity checks during development.
Try It Yourself
Good error handling makes your programs reliable, readable, and easier to maintain. Instead
of letting programs crash or hiding bugs, you should follow certain best practices.
Deep Dive
1. Be Specific in except Blocks Catch only the exceptions you expect, not all of them.
try:
num = int("abc")
except ValueError:
print("Invalid number!") # good
Avoid:
167
except:
print("Something went wrong") # too vague
2. Use finally for Cleanup Always free resources like files, network connections, or
databases.
try:
f = open("data.txt")
content = f.read()
except FileNotFoundError:
print("File not found.")
finally:
f.close()
3. Keep try Blocks Small Put only the risky code inside try, not everything.
try:
result = 10 / 0
except ZeroDivisionError:
print("Math error")
4. Don’t Hide Bugs Catching all exceptions with except Exception should be a last resort.
Otherwise, real bugs get hidden.
5. Raise Exceptions When Needed Instead of returning special values like -1, raise meaningful
errors.
6. Create Custom Exceptions for Clarity For domain-specific logic, define your own exceptions
(e.g., PasswordTooShortError).
7. Log Errors Use Python’s logging module instead of just print().
import logging
logging.error("File not found", exc_info=True)
168
Quick Summary Table
Tiny Code
print(safe_divide(10, 2))
print(safe_divide(5, 0)) # raises ValueError
Why it Matters
Well-structured error handling prevents small mistakes from becoming big failures. It keeps
your programs predictable, professional, and easier to debug.
Try It Yourself
169
Chapter 9. Advanced Python Features
A list comprehension is a concise way to create lists in Python. It lets you generate new lists
by applying an expression to each item in an existing sequence (or iterable), often replacing
loops with a single readable line.
Deep Dive
Basic Syntax
Example:
nums = [1, 2, 3, 4]
squares = [x2 for x in nums]
print(squares) # [1, 4, 9, 16]
With a Condition
With Functions
170
squares = []
for x in range(5):
squares.append(x2)
Comprehension version:
Form Example
Simple comprehension [x*2 for x in range(5)]
With condition [x for x in range(10) if x % 2 == 0]
Nested loops [(x,y) for x in [1,2] for y in [3,4]]
With function [f(x) for x in items]
Tiny Code
nums = [1, 2, 3, 4, 5]
double = [n * 2 for n in nums if n % 2 != 0]
print(double) # [2, 6, 10]
Why it Matters
List comprehensions make your code shorter, faster, and easier to read. They are a hallmark of
Pythonic style, turning loops and conditions into expressive one-liners.
Try It Yourself
171
82. Dictionary Comprehensions
Deep Dive
Basic Syntax
Example:
nums = [1, 2, 3, 4]
squares = {x: x2 for x in nums}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16}
With a Condition
With Functions
172
Quick Summary Table
Form Example
Basic dict comp {x: x*2 for x in range(3)}
With condition {x: x2 for x in range(6) if x % 2 == 0}
Swap keys and values {v: k for k, v in dict.items()}
Using function {w: len(w) for w in words}
Nested loops {(x,y): x*y for x in A for y in B}
Tiny Code
Why it Matters
Dictionary comprehensions save time and reduce boilerplate when building mappings from
existing data. They make code cleaner, more expressive, and Pythonic.
Try It Yourself
Deep Dive
Basic Syntax
173
{expression for item in iterable}
Example:
nums = [1, 2, 2, 3, 4, 4]
unique_squares = {x2 for x in nums}
print(unique_squares) # {16, 1, 4, 9}
With a Condition
From a String
With Functions
Form Example
Simple set comp {x*2 for x in range(5)}
With condition {x for x in range(10) if x % 2 == 0}
From string {ch for ch in "banana"}
With function {len(w) for w in words}
Nested loops {(x,y) for x in A for y in B}
174
Tiny Code
nums = [1, 2, 3, 2, 1, 4]
squares = {n2 for n in nums if n % 2 != 0}
print(squares) # {1, 9}
Why it Matters
Set comprehensions provide a quick way to eliminate duplicates and apply transformations at
the same time. They’re useful for data cleaning, filtering, and fast membership checks.
Try It Yourself
A generator is a special type of function that lets you produce a sequence of values lazily, one at
a time, using the yield keyword. Unlike regular functions, generators don’t return everything
at once—they pause and resume.
Deep Dive
Basic Generator
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
Output:
175
1
2
3
4
5
gen = count_up_to(3)
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
def even_numbers():
n = 0
while True:
yield n
n += 2
gen = even_numbers()
for _ in range(5):
print(next(gen)) # 0 2 4 6 8
176
Feature Example Behavior
Generator function def f(): yield ... Creates a generator
Generator expr (x2 for x in range(5)) Compact generator syntax
Tiny Code
def fibonacci(limit):
a, b = 0, 1
while a <= limit:
yield a
a, b = b, a + b
Why it Matters
Generators are memory-efficient because they don’t build the whole list in memory. They’re
ideal for large datasets, streams of data, or infinite sequences.
Try It Yourself
85. Iterators
An iterator is an object that represents a stream of data. It returns items one at a time when
you call next() on it, and it remembers its position between calls. Iterators are the foundation
of loops, comprehensions, and generators in Python.
177
Deep Dive
Built-in Iterators
nums = [1, 2, 3]
it = iter(nums) # get iterator
print(next(it)) # 1
print(next(it)) # 2
print(next(it)) # 3
# next(it) now raises StopIteration
is equivalent to:
nums = [1, 2, 3]
it = iter(nums)
while True:
try:
print(next(it))
except StopIteration:
break
Custom Iterator You can build your own iterator by defining __iter__ and __next__:
class CountDown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
178
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
Output:
5
4
3
2
1
Tiny Code
print(next(it)) # 10
print(next(it)) # 20
print(next(it)) # 30
Why it Matters
Understanding iterators explains how loops, generators, and comprehensions actually work in
Python. Iterators allow Python to handle large datasets efficiently, consuming one item at a
time.
179
Try It Yourself
1. Use iter() and next() on a string like "hello" to get characters one by one.
2. Build a simple custom iterator that counts from 1 to 5.
3. Write a for loop manually using while True and next() with StopIteration.
4. Create a custom iterator EvenNumbers(n) that yields even numbers up to n.
86. Decorators
A decorator is a special function that takes another function as input, adds extra behavior to it,
and returns a new function. In Python, decorators are often used for logging, authentication,
caching, and more.
Deep Dive
Basic Decorator
def my_decorator(func):
def wrapper():
print("Before function runs")
func()
print("After function runs")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
180
def repeat(func):
def wrapper():
for _ in range(3):
func()
return wrapper
@repeat
def greet():
print("Hi!")
greet()
Output:
Hi!
Hi!
Hi!
def log_args(func):
def wrapper(*args, kwargs):
print("Arguments:", args, kwargs)
return func(*args, kwargs)
return wrapper
@log_args
def add(a, b):
return a + b
print(add(3, 5))
Using functools.wraps Without it, the decorated function loses its original name and doc-
string.
def decorator(func):
@wraps(func)
def wrapper(*args, kwargs):
return func(*args, kwargs)
return wrapper
181
Quick Summary Table
Tiny Code
def uppercase(func):
def wrapper():
result = func()
return result.upper()
return wrapper
@uppercase
def message():
return "hello world"
Why it Matters
Decorators are a powerful way to separate what a function does from how it’s used. They make
code reusable, clean, and Pythonic.
Try It Yourself
1. Write a decorator @timer that prints how long a function takes to run.
2. Create a decorator @authenticate that prints "Access denied" unless a variable
user_logged_in = True.
3. Combine two decorators on the same function and observe the order of execution.
4. Use functools.wraps to keep the function’s original __name__.
182
87. Context Managers (Custom)
A context manager is a Python construct that properly manages resources, like opening and
closing files. You usually use it with the with statement. While Python has built-in context
managers (like open), you can also create your own.
Deep Dive
Here, open is a context manager: it opens the file, then automatically closes it when done.
Creating a Custom Context Manager with a Class To make your own, define __enter__ and
__exit__.
class MyResource:
def __enter__(self):
print("Resource acquired")
return self
with MyResource() as r:
print("Using resource")
Output:
Resource acquired
Using resource
Resource released
183
class SafeDivide:
def __enter__(self):
return self
with SafeDivide():
print(10 / 0) # No crash!
@contextmanager
def managed_resource():
print("Start")
yield
print("End")
with managed_resource():
print("Inside block")
Output:
Start
Inside block
End
Tiny Code
184
from contextlib import contextmanager
@contextmanager
def open_upper(filename):
f = open(filename, "r")
try:
yield (line.upper() for line in f)
finally:
f.close()
Why it Matters
Custom context managers let you manage setup and cleanup tasks automatically. They make
code safer, reduce errors, and ensure resources are always released properly.
Try It Yourself
1. Write a context manager class that prints "Start" when entering and "End" when exiting.
2. Create one that temporarily changes the working directory and restores it afterwards.
3. Use @contextmanager to make a timer context that prints how long the block took.
4. Build a safe database connection context that opens, yields, then closes automatically.
The with statement in Python is a shortcut for using context managers. It ensures resources
(like files, network connections, or locks) are acquired and released properly, even if errors
occur.
Deep Dive
185
• File opens automatically.
• File closes automatically after the block, even if an error happens.
Both files are managed safely within the same with statement.
Using with for Locks (Threading Example)
import threading
lock = threading.Lock()
with lock:
# critical section
print("Safe access")
import sqlite3
class Resource:
def __enter__(self):
print("Acquired resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Released resource")
with Resource():
print("Using resource")
186
Output:
Acquired resource
Using resource
Released resource
Tiny Code
Why it Matters
Resource management is crucial to avoid memory leaks, file corruption, or dangling connections.
The with statement makes code safer, cleaner, and more professional.
Try It Yourself
Python provides itertools and functools as standard libraries to work with iterators and
functional programming tools. They let you process data efficiently and write more expressive
code.
187
Deep Dive
• Infinite Iterators
import itertools
repeat_hello = itertools.repeat("hello", 3)
print(list(repeat_hello)) # ['hello', 'hello', 'hello']
• Combinatorics
• Chaining Iterables
188
from functools import reduce
nums = [1, 2, 3, 4]
product = reduce(lambda a, b: a * b, nums)
print(product) # 24
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
189
Tiny Code
Why it Matters
itertools and functools give you powerful building blocks for iteration and function manip-
ulation. They make complex tasks simpler, faster, and more memory-efficient.
Try It Yourself
Type hints let you specify the expected data types of variables, function arguments, and return
values. They don’t change how the code runs, but they make it easier to read, maintain, and
catch errors early with tools like mypy.
Deep Dive
Variable Hints
age: int = 25
pi: float = 3.14159
active: bool = True
190
Using List, Dict, and Tuple
Optional Values
Union Types
Type Aliases
UserID = int
def get_user(id: UserID) -> str:
return "User" + str(id)
print(apply(lambda x, y: x + y, 2, 3)) # 5
191
Type Hint Example Meaning
Basic x: int, def f()->str Simple types
List, Dict List[int], Dict[str,int] Collections with types
Tuple Tuple[int,str] Fixed-size sequence
Optional Optional[str] String or None
Union Union[int,float] One of several types
Callable Callable[[int,int],int] Function type
Alias UserID = int Custom type name
Tiny Code
Why it Matters
Type hints improve clarity and enable better error detection during development. They help
teams understand code faster and catch mistakes before running the program.
Try It Yourself
Python comes with an interactive environment called the REPL (Read–Eval–Print Loop). It
lets you type Python commands one at a time and see results immediately, making it perfect
192
for learning, testing, and quick experiments.
Deep Dive
python
or sometimes:
python3
>>>
>>> 2 + 3
5
>>> "hello".upper()
'HELLO'
The REPL evaluates each expression and prints the result instantly.
Multi-line Input For blocks like loops or functions, use indentation:
>>> help(str)
>>> dir(list)
193
>>> 5 * 5
25
>>> _ + 10
35
Enhanced REPLs
Tiny Code
>>> x = 10
>>> y = 20
>>> x + y
30
>>> _
30
>>> _ * 2
60
Why it Matters
The REPL makes Python beginner-friendly and powerful for professionals. It’s like a live
scratchpad where you can test ideas, debug small snippets, or explore libraries interactively.
194
Try It Yourself
Python includes a built-in debugger called pdb. It allows you to pause execution, step through
code line by line, inspect variables, and find bugs interactively.
Deep Dive
Starting the Debugger Insert this line where you want to pause:
When the program runs, it will stop there and open an interactive debugging session.
Common pdb Commands
Command Meaning
n Next line (step over)
s Step into a function
c Continue until next breakpoint
l List source code around current line
p var Print the value of var
q Quit the debugger
b num Set a breakpoint at line number num
x = 10
y = 0
195
import pdb; pdb.set_trace()
print(divide(x, y))
When run:
(Pdb) p x
10
(Pdb) p y
0
(Pdb) n
ZeroDivisionError: division by zero
Running a Script with Debug Mode You can also run the debugger directly from the command
line:
Modern Alternatives
Tiny Code
def greet(name):
message = "Hello " + name
return message
(Pdb) p name
(Pdb) n
196
Why it Matters
Debugging with pdb helps you see exactly what your program is doing step by step. Instead of
guessing where things go wrong, you can inspect state directly and fix issues faster.
Try It Yourself
1. Write a function that divides two numbers and insert pdb.set_trace() before the
division. Step through and print variables.
2. Run a script with python -m pdb file.py and use n and s to move through code.
3. Try setting a breakpoint with b and continuing with c.
4. Experiment with inspecting variables using p var during debugging.
The logging module in Python is used to record messages about what your program is doing.
Unlike print(), logging is flexible, configurable, and suitable for real-world applications.
Deep Dive
Basic Logging
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Program started")
logging.warning("This is a warning")
logging.error("An error occurred")
Output:
INFO:root:Program started
WARNING:root:This is a warning
ERROR:root:An error occurred
197
Level Function Meaning
DEBUG logging.debug() Detailed information for devs
INFO logging.info() General program information
WARNING logging.warning() Something unexpected happened
ERROR logging.error() A serious problem occurred
CRITICAL logging.critical() Very severe error
Custom Formatting
logging.basicConfig(
format="%(asctime)s - %(levelname)s - %(message)s",
level=logging.DEBUG
)
logging.debug("Debugging details")
Example output:
Logging to a File
logging.basicConfig(filename="app.log", level=logging.INFO)
logging.info("This message goes into the log file")
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
logger.info("App is running")
198
Quick Summary Table
Tiny Code
import logging
logging.basicConfig(level=logging.WARNING)
logging.debug("Hidden")
logging.warning("Visible warning")
Why it Matters
Logging is essential for debugging, monitoring, and auditing applications. It helps you under-
stand what your code does in production without spamming users with print statements.
Try It Yourself
1. Write a script that logs an INFO message when it starts and an ERROR when something
goes wrong.
2. Change log formatting to include the date and time.
3. Configure logging to write output to a file instead of the console.
4. Create two different loggers: one for db and one for api, with different log levels.
Python’s unittest module provides a framework for writing and running automated tests. It
helps you verify that your code works as expected and prevents future changes from breaking
existing functionality.
199
Deep Dive
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
if __name__ == "__main__":
unittest.main()
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Common Assertions
Testing Exceptions
200
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_zero_division(self):
with self.assertRaises(ValueError):
divide(5, 0)
class TestStrings(unittest.TestCase):
def test_upper(self):
self.assertEqual("hello".upper(), "HELLO")
def test_isupper(self):
self.assertTrue("HELLO".isupper())
self.assertFalse("Hello".isupper())
Running Tests
• Run directly:
python test_file.py
• Or use:
Tiny Code
201
import unittest
class TestBasics(unittest.TestCase):
def test_sum(self):
self.assertEqual(sum([1,2,3]), 6)
if __name__ == "__main__":
unittest.main()
Why it Matters
Unit tests catch bugs early, make code safer to change, and provide confidence that your
program works correctly. They are a cornerstone of professional software development.
Try It Yourself
A virtual environment is an isolated Python environment that allows you to install packages
without affecting the system-wide Python installation. It’s the best practice for managing
dependencies in projects.
Deep Dive
202
This creates a folder venv/ that holds the environment.
Activating the Virtual Environment
• Linux / macOS:
source venv/bin/activate
• Windows (cmd):
venv\Scripts\activate
(venv) $
deactivate
Best Practices
203
Quick Summary Table
Command Purpose
python -m venv venv Create environment
source venv/bin/activate Activate (Linux/macOS)
venv\Scripts\activate Activate (Windows)
pip install package Install inside environment
pip freeze > requirements.txt Save dependencies
deactivate Exit environment
Tiny Code
Why it Matters
Virtual environments prevent dependency chaos. They ensure that one project’s libraries don’t
break another’s, making projects portable and maintainable.
Try It Yourself
Python scripts are just plain text files with .py extension. They can contain functions, logic,
and be executed directly from the command line.
204
Deep Dive
Run it:
python hello.py
Using if __name__ == "__main__": This ensures some code only runs when the file is
executed directly, not when imported as a module.
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
print(greet("Alice"))
Hello, Alice!
import hello
print(hello.greet("Bob"))
import sys
Run it:
205
python script.py Alice
# Output: Hello, Alice!
#!/usr/bin/env python3
chmod +x hello.py
./hello.py
Tiny Code
import sys
if __name__ == "__main__":
num = int(sys.argv[1]) if len(sys.argv) > 1 else 5
print(f"Square of {num} is {square(num)}")
Why it Matters
Scripts turn Python into a tool for automation. With just a few lines, you can create utilities,
batch jobs, or prototypes that are reusable and shareable.
206
Try It Yourself
Python’s argparse module makes it easy to build user-friendly command-line interfaces (CLI).
Instead of manually reading sys.argv, you can define arguments, defaults, help text, and
parsing rules automatically.
Deep Dive
Basic Example
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
args = parser.parse_args()
print(f"Hello, {args.name}!")
Run:
Optional Arguments
parser = argparse.ArgumentParser()
parser.add_argument("--age", type=int, default=18, help="Your age")
args = parser.parse_args()
print(f"Age: {args.age}")
Run:
207
python script.py --age 25
# Output: Age: 25
Multiple Arguments
parser = argparse.ArgumentParser()
parser.add_argument("x", type=int)
parser.add_argument("y", type=int)
args = parser.parse_args()
print(args.x + args.y)
Run:
python script.py 5 7
# Output: 12
Flags (True/False)
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
if args.verbose:
print("Verbose mode on")
Run:
208
Tiny Code
import argparse
print(args.num 2)
Run:
python script.py 6
# Output: 36
Why it Matters
With argparse, your Python scripts behave like real command-line tools. They’re more
professional, self-documenting, and easier to use.
Try It Yourself
APIs let programs talk to each other over the web. Python’s requests library makes sending
HTTP requests simple and readable. You can use it to fetch data, send data, or interact with
web services.
Deep Dive
209
import requests
response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
print(response.status_code) # 200 means success
print(response.json()) # Parse response as JSON
Query Parameters
url = "https://jsonplaceholder.typicode.com/posts"
params = {"userId": 1}
response = requests.get(url, params=params)
print(response.json()) # All posts from userId=1
Handling Errors
response = requests.get("https://jsonplaceholder.typicode.com/invalid")
if response.status_code != 200:
print("Error:", response.status_code)
210
Tiny Code
import requests
r = requests.get("https://api.github.com")
print("Status:", r.status_code)
print("Headers:", r.headers["content-type"])
Why it Matters
APIs are everywhere—from weather apps to payment systems. Knowing how to interact with
them lets you integrate external services into your projects.
Try It Yourself
Web scraping means extracting information from websites automatically. In Python, this
is commonly done using requests to fetch the page and BeautifulSoup (bs4) to parse the
HTML.
Deep Dive
Installing BeautifulSoup
Fetching a Webpage
211
import requests
from bs4 import BeautifulSoup
url = "https://example.com"
response = requests.get(url)
soup = BeautifulSoup(response.text, "html.parser")
Extracting Data
print(soup.title.string)
print(soup.p.text)
soup.find_all("div", class_="article")
url = "https://news.ycombinator.com"
res = requests.get(url)
soup = BeautifulSoup(res.text, "html.parser")
212
Quick Summary Table
Tiny Code
import requests
from bs4 import BeautifulSoup
res = requests.get("https://example.com")
soup = BeautifulSoup(res.text, "html.parser")
print("Title:", soup.title.string)
print("First paragraph:", soup.p.text)
Why it Matters
Web scraping lets you automate data collection from websites—useful for research, market
analysis, or building datasets when APIs aren’t available.
Try It Yourself
Now that you’ve mastered the Python flashcards, you have the foundation to build almost
anything. The next step is to choose a direction and deepen your skills in areas that interest
you most.
213
Deep Dive
2. Web Development
• Use Python to automate repetitive tasks (file handling, Excel reports, web scraping).
• Explore selenium for browser automation.
Learning Pathways
• Books: Fluent Python, Automate the Boring Stuff with Python, Python Crash Course.
• Online platforms: Coursera, edX, freeCodeCamp.
• Open-source projects: contribute on GitHub to gain real experience.
214
Direction Libraries / Tools Example Goal
CS Foundations heapq, collections Implement algorithms in Python
import requests
def get_weather(city):
url = f"https://wttr.in/{city}?format=3"
res = requests.get(url)
return res.text
print(get_weather("London"))
Why it Matters
Python is not just a language—it’s a gateway. Whether you’re interested in AI, finance, web
apps, or automating your own life, Python is a tool that grows with you.
Try It Yourself
215