Python Without Gaps: Zero to Advanced

A Complete Guide for Beginners, Professionals, enthusiasts & Builders. Practical Notes, Tutorials and Exercises

Author

Prosper AYINEBYONA

Published

November 13, 2025

1 Introduction

Python has rapidly become one of the most popular programming languages in the world, with growth unmatched in computing history. It’s highly versatile, used for scripting, automating tasks, web development, data analysis, machine learning, game creation, and even programming embedded systems.

It has also become the top choice for teaching computer science fundamentals in universities worldwide. Millions of beginners start with Python, and for many, it remains their main language due to its simplicity and broad capabilities.

Python’s appeal comes from its clear, easy-to-read syntax, large and supportive community, and extensive library ecosystem that covers nearly every imaginable task. It is a high-level language that balances ease of learning with professional utility.

Technically, Python is an interpreted and dynamically typed language. This means it executes code directly without a separate compilation step, and variable types are determined at runtime. While this speeds up development, it can also lead to runtime errors that compiled languages might catch earlier.

Python supports multiple programming paradigms, procedural, object-oriented, and functional, making it adaptable for a wide range of uses.

This course is carefully crafted and structured into 41 comprehensive chapters, taking learners from absolute fundamentals to advanced, professional-level Python concepts. Whether you are writing your first line of code or refining advanced skills, this course is designed to grow with you.

Created by Guido van Rossum in 1991, Python’s popularity has surged dramatically in recent years as this Google Trends infographic shows:

1.1 Who This Course Is For

This course is intentionally designed to be inclusive and accessible, making it suitable for a wide and diverse audience, including:

  • Absolute beginners with no prior programming experience

  • Working professionals looking to automate tasks, improve productivity, or switch careers

  • Job seekers preparing for technical roles that require Python skills

  • University Students and recent graduates learning programming or computer science fundamentals

  • Developers and software engineers who want a strong, complete understanding of Python

  • Data enthusiasts interested in data analysis, visualization, and machine learning foundations

  • Researchers and analysts using Python for experimentation and problem solving

  • Entrepreneurs and startups building products and prototypes

  • Hobbyists and self-learners exploring programming for personal projects or curiosity

No matter your background, experience level, or goal, this course provides a clear, structured path to confidently using Python in real-world scenarios.

1.2 Why This Course?

  • Starts from zero and progresses to advanced concepts

  • Covers almost every major aspect of Python

  • Designed for both learning and practical application

  • Suitable for academic, professional, and personal use

  • One complete course instead of multiple fragmented resources

  • Certificate of completion awarded upon successfully finishing the course, adding value to your resume and professional profile

By the end of this course, learners will not only understand Python, they will be able to think in Python, apply it confidently, and use it as a powerful tool for real world problem solving.

1.3 Getting Started

Getting started with Python is simple. You just need to download and install the official package from python.org, available for Windows, macOS, or Linux, and you can begin right away.

If you’re completely new to programming, the upcoming lessons will help you progress from a beginner to a capable Python programmar.

Even for experienced programmers who primarily use other languages, learning Python is highly worthwhile, as its popularity and influence continue to rise.

While lower level languages like C++ or Rust are powerful tools for expert programmers, they can be intimidating for beginners and require significant time to master.

Python, by contrast, is designed for everyone, students, people doing their day jobs with Excel, scientists, and more.

Python is the language everyone interested in coding should learn first.

1.4 How to Install Python

Go to https://www.python.org, choose the Downloads menu, choose your operating system, and a panel with a link to download the official package will appear:

Be sure to follow the installation steps that match your operating system. If you’re using macOS, you can check out a comprehensive setup guide at Installing Python 3 on macOS

1.5 How to Run Python Programs

There are a few different ways to run Python programs.

In particular, there’s a distinction between using interactive prompts, where you type Python code and it’s immediately executed, and saving a Python program into a file and executing that.

Let’s start with interactive prompts.

If you open your terminal and type python, you will see a screen like this:

This is the Python REPL (Read-Evaluate-Print-Loop).

Notice the >>> symbol, and the cursor after that. You can type any Python code here, and press the enter key to run it.

For example try defining a new variable using

Show/Hide Code
name = "Prosper"

and then print its value, using print():

Show/Hide Code
print(name)
Prosper

Note

In the REPL, you can also just type name, press the enter key and you’ll get the value back. But in a program, you are not going to see any output if you do so - you need to use print() instead.

Any line of Python you write here is going to be executed immediately.

Type quit() to exit this Python REPL.

You can access the same interactive prompt using the IDLE application that’s installed by Python automatically:

This approach might be easier since using a mouse lets you navigate and copy or paste text more conveniently than working directly in the terminal.

The tools mentioned so far are included with Python by default. However, I suggest installing IPython, which is widely regarded as one of the best interactive command-line REPL environments available.

You can install it by running:

Show/Hide Code
pip install ipython

Make sure the pip binaries are in your path, then run ipython:

ipython is another interface that lets you work with a Python REPL, and provides some nice features like syntax highlighting, code completion, and much more.

The second way to run a Python program is to write your Python program code into a file, for example first_python_file.py:

and then run it with python first_python_file.py:

Note

Note that we save Python programs with the .py extension - that’s a convention.

In this case the program is executed as a whole, not one line at a time. And that’s typically how we run programs.

We use the REPL for quick prototyping and for learning.

On Linux and macOS, a Python program can also be transformed into a shell script, by prepending all its content with a special line that indicates which executable to use to run it.

We have many other ways to run Python programs.

One of them is using VS Code, and in particular the official Python extension from Microsoft:

After installing this extension you will have Python code autocompletion and error checking, automatic formatting and code linting with pylint, and some special commands, including:

Python: Start REPL to run the REPL in the integrated terminal:

Python: Run Python File in Terminal to run the current file in the terminal:

and many more. Just open the Command Palette (View → Command Palette or press Ctrl + Shift + P) and type “Python” to see all the Python-related commands.

Another way to easily run Python code is to use replit, a very nice website that provides a coding environment you can create and run your apps on, in any language, Python included:

Signup (it’s free)!

I think replit is handy because:

  • you can easily share code just by sharing the link

  • multiple people can work on the same code

  • it can host long-running programs

  • you can install packages

  • it provides you a key-value database for more complex applications

Jupyter Notebook

Jupyter Notebook is one of the most popular tools for learning and practicing Python, especially for data science, statistics, and research.

It allows you to mix code, text, and visualizations in one interactive document.

1.5.0.1 What is Jupyter Notebook?

  • It runs in your web browser.

  • You can write Python code in small cells and run them individually.

  • It displays immediate output (including charts, tables, and markdwn notes).

Example of a cell inside Jupyter:

Show/Hide Code
# Let's try it!
name = "Prosper"
print(f"Hello, {name}! Welcome to Jupyter Notebook.")
Hello, Prosper! Welcome to Jupyter Notebook.

Installing Jupyter Notebook (via Anaconda)

The easiest way to install Jupyter Notebook is through Anaconda, a free Python distribution that includes:

  • Python itself

  • Jupyter Notebook

  • Common data science libraries (NumPy, pandas, Matplotlib, etc.)

Step-by-Step Installation Guide

  1. Go to the official Anaconda website: 👉 https://www.anaconda.com/download

  2. Download the version for your operating system (Windows, macOS, or Linux).

  3. Run the installer and follow the default instructions, it will install both Anaconda and Python.

Once installed, you can verify it by opening your terminal (or Anaconda Prompt on Windows) and typing:

conda --version

If you see a version number, Anaconda was installed successfully!

Launching Jupyter Notebook

You can launch Jupyter Notebook in two ways:

Option 1: From Anaconda Navigator

  • Open Anaconda Navigator from your Start Menu or Applications folder.

  • Click on the “Launch” button under Jupyter Notebook.

  • It will open in your default web browser.

Option 2: From Terminal or Command Prompt

  1. Open your terminal (or Anaconda Prompt).

  2. Navigate to the folder where you want to work. Example:

    cd folder1
  3. then run:

    jupyter notebook
  4. Your web browser will open automatically, showing a dashboard. From there, click New → Python 3 (ipykernel) to create a new notebook.

Try it Yourself

  1. Create a new notebook called first_notebook.ipynb.

  2. In the first cell, write and run:

Show/Hide Code
print("Hello from my first Jupyter Notebook!")
Hello from my first Jupyter Notebook!
  1. In the next cell, try:
Show/Hide Code
for i in range(1, 6):
    print("Number:", i)
Number: 1
Number: 2
Number: 3
Number: 4
Number: 5

You’ve just executed your first Python notebook!

Note
  • Save your notebook often using Ctrl + S or File → Save and Checkpoint.

  • Rename your notebook at the top (e.g., Intro_to_Python.ipynb).

1.6 Python 2 vs Python 3

A key topic we should cover right away is the Python 2 vs. Python 3 debate.

Python 3 was released in 2008 and has been the main focus of ongoing development ever since, while Python 2 continued to receive bug fixes and security updates until early 2020.

At that point, official support for Python 2 ended.

Many existing programs are still based on Python 2, and some organizations continue maintaining them, as migrating to Python 3 can be complex and time consuming, and large scale upgrades often risk introducing new bugs.

However, any new code should be written in Python 3, unless specific organizational policies require the use of Python 2.

Our focus is on Python 3.

2 Python Basics

2.1 Variables in Python

In Python, variables are like containers that hold data, numbers, text, lists, or anything else your program needs to remember and use later.

What Is a Variable?

A variable is a name that refers to a value stored in memory.
You create a variable using the assignment operator =.

Let’s see it in action:

Show/Hide Code
# Assign a string (text) to a variable
name = "Prosper"

# Assign a number to a variable
age = 7

# Display the variables
print(name)
print(age)
Prosper
7

Here:

  • name and age are variable names (also called identifiers).

  • "Prosper" and 7 are values stored in those variables.

  • The = operator assigns the value on the right to the variable on the left.

Variables Can Change

Unlike constants, variables can be updated:

Show/Hide Code
# Initial assignment
city = "Kigali"
print(city)

# Reassign a new value
city = "Musanze"
print(city)
Kigali
Musanze

The variable city first stored "Kigali", then it was reassigned to "Musanze".

This is why we call them variables , their value can vary.

Rules for Naming Variables

A variable name can include:

  • Letters (A–Z, a–z)

  • Numbers (0–9)

  • Underscores _

But there are rules you must follow:

Valid variable names:

Show/Hide Code
name1 = "Alice"
AGE = 25
aGE = 26
a11111 = 11111
my_name = "Prosper"
_name = "Hidden variable"

Invalid variable names:

Show/Hide Code
123 = "Invalid"     # Cannot start with a number
test! = "Invalid"   # No special characters like !, %, $, etc.
name% = "Invalid"   # % not allowed

2.1.1 Reserved Keywords

You cannot name a variable using Python’s reserved words (keywords).
These are special words that have meaning in Python, such as:

for, if, else, while, import, True, False, None, etc.

Python will stop you if you try to use them:

Show/Hide Code
if = 5
💡 Tip

You can see all Python keywords by running this:

Show/Hide Code
import keyword
print(keyword.kwlist)

2.1.2 Variable Naming Best Practices (PEP 8 Style)

  • Use lowercase_with_underscores for variable names:
    user_name, total_price, student_age

  • Avoid short or confusing names like x, y, z unless in math examples.

  • Use descriptive names that explain what they store.

  • Python is case-sensitive, so Name and name are different variables.

Example:

Show/Hide Code
userName = "Alice"  # Works, but not recommended
user_name = "Alice" # ✅ Recommended (PEP 8 style)
📚Try It Yourself

Create a small Python script that defines three variables — your name, age, and favorite hobby — and prints a sentence about you.

Example:

Show/Hide Code
name = "Prosper"
age = 7
hobby = "eating"

print(f"My name is {name}, I am {age} years old and I love {hobby}.")

2.2 Expressions and statements in Python

In Python, your code is made up of expressions and statements, the building blocks of every program.

What’s an Expression?

An expression is any piece of code that produces a value.
When you type an expression in Python, it’s evaluated and returns a result.

Examples:

Show/Hide Code
1 + 1
"Prosper"
3 * 4
len("Football")
8

Each of these expressions produces a value. You can also use expressions inside functions, assignments, or other statements.

Show/Hide Code
age = 5 + 2  # 5 + 2 is an expression
print(age)    # statement that prints 7
7

What’s a Statement?

A statement is a complete instruction that Python can execute.
It might perform an action, such as assigning a value or printing output.

Examples:

Show/Hide Code
name = "Prosper"   # Assignment statement
print(name)      # Function call statement
Prosper

Each line above is a statement, something Python runs to “do” something.

Combining Statements

A Python program is simply a series of statements that execute one after another.

Example:

Show/Hide Code
name = "Prosper"
age = 7
print(name)
print(age)
Prosper
7

You can even write multiple statements on the same line using a semicolon ;, though it’s not recommended (for readability):

Show/Hide Code
name = "Prosper"; print(name)
Prosper
💡Best practice

Always put each statement on its own line., it’s clearer and easier to debug.

2.3 Comments in Python

Sometimes, you’ll want to write notes in your code to explain what it does. Python lets you do this with comments, lines that Python ignores when running your program.

Single-Line Comments

Use the # symbol to write a comment. Everything after # is ignored.

Show/Hide Code
# This is a comment
name = "Prosper"  # This is an inline comment
print(name)     # Display the name

Comments are great for:

  • Explaining what your code does

  • Leaving reminders for yourself or others

  • Temporarily disabling lines of code

Example:

Show/Hide Code
# print("This won't run")
print("This will run")

Multi-line Comments (Docstring Style)

Python doesn’t have a special syntax for multi-line comments,
but you can use triple quotes (''' or """) as a common workaround for large notes:

Show/Hide Code
"""
This section of code prints
a friendly message to the user.
"""
print("Welcome to Python")
Welcome to Python

2.4 Indentation in Python

Unlike many languages, indentation (spaces or tabs at the start of a line) is part of Python’s syntax.
Python uses indentation to define code blocks (sections of code that belong together).

Why Indentation Matters

In languages like C or JavaScript, code blocks are marked with {} brackets.
In Python, indentation replaces those braces.

Example:

Show/Hide Code
name = "Prosper"
  print(name)

If you run this, sometimes Python gives an error IndentationError: unexpected indent

That’s because the second line is indented without reason, Python thinks you’re trying to start a block.

Correct Indentation Example

Show/Hide Code
name = "Prosper"
print(name)
Prosper

Now the indentation is consistent, both lines start at the same level.

Indentation Defines Code Blocks

When you write control structures like if, for, or while,
you must indent the code inside those blocks.

Example:

Show/Hide Code
name = "Prosper"

if name == "Prosper":
    print("Hello, Prosper")  # indented = inside the if block
    print("Welcome back!")  # still inside the block

print("Program finished")   # not indented = outside the block
Hello, Prosper
Welcome back!
Program finished
📚Try it

Try to fix this code and make it run without errors:

Show/Hide Code
x = 5
if x > 3:
print("x is greater than 3")

3 Data Types in Python

Everything in Python is an object, and every object has a type. Data types tell Python what kind of data a variable holds and what operations can be performed on it.

3.1 What Are Data Types?

Python automatically assigns a data type to a variable when you create it.
You don’t need to declare the type explicitly — Python figures it out for you.

Let’s see this in action

Show/Hide Code
# Assign a string to a variable
name = "Prosper"

# Assign a number to a variable
age = 7

# Display their types
print(type(name))
print(type(age))
<class 'str'>
<class 'int'>

Here:

  • "Prosper" is a string (text).

  • 7 is an integer (whole number).

3.2 Checking the Type of a Variable

Python provides two useful built-in functions for checking types:

  1. Using type()

The type() function returns the class (type) of an object.

Show/Hide Code
name = "Prosper"
print(type(name))  # True
<class 'str'>

Using isinstance()

isinstance() checks if a variable is an instance of a specific type.

Show/Hide Code
name = "Prosper"
print(isinstance(name, str))  # True
print(isinstance(name, int))  # False
True
False

3.3 Numeric Types in Python

Python has three main numeric types:

Type Description Example
int Whole numbers 10, -5, 7, 0
float Decimal (fractional) numbers 3.14, 0.5, -10.2
complex Complex numbers 2 + 3j, 5j

Examples

Show/Hide Code
# Integer
age = 7
print(type(age))  # <class 'int'>

# Float
fraction = 0.1
print(type(fraction))  # <class 'float'>

# Complex number
z = 2 + 3j
print(type(z))  # <class 'complex'>
<class 'int'>
<class 'float'>
<class 'complex'>

3.4 Type Conversion (Casting)

You can convert (or “cast”) between types using the class constructor functions like int(), float(), and str().

Example 1: Converting String to Integer

Show/Hide Code
age = int("7")
print(age)
print(type(age))
7
<class 'int'>

Example 2: Converting Float to Integer

Show/Hide Code
fraction = 0.1
int_fraction = int(fraction)
print(int_fraction)
0
Be carefull

When you convert a float to an int, the decimal part is truncated, not rounded.

3.4.0.1 Conversion Errors

Not every string can be converted to a number:

Show/Hide Code
age = int("test")

Output: ValueError: invalid literal for int() with base 10: 'test'

That’s because "test" isn’t a number.

3.5 Creating Specific Types Using Constructors

You can create values explicitly using their class constructors:

Show/Hide Code
name = str("Prosper")
another_name = str(name)

age = int("7")
price = float("9.99")

Python automatically detects types when you assign literals, but constructors are useful for conversions and clarity.

3.6 Summary of Common Built-in Types

Here’s a quick overview of the most common built-in types in Python:

Type Description Example
int Integer (whole number) x = 42
float Decimal number y = 3.14
complex Complex number z = 2 + 3j
str String (text) name = "Roger"
bool Boolean (True/False) is_active = True
list Ordered, mutable collection colors = ["red", "green"]
tuple Ordered, immutable collection point = (10, 20)
dict Key–value mapping person = {"name": "Roger", "age": 8}
set Unordered, unique items numbers = {1, 2, 3}
range Sequence of numbers range(0, 10)
NoneType Represents “no value” value = None
📚Try it
  1. Create a variable called height and assign it the value 1.75 (in meters).

  2. Create a variable called weight and assign it the value "65".

  3. Convert weight to an integer and print your BMI using:

Show/Hide Code
height = 1.75
weight = "65"

weight = int(weight)
bmi = weight / (height ** 2)

print("my BMI is:", bmi)
my BMI is: 21.224489795918366

4 Operators in Python

Operators are symbols or keywords used to perform operations on values and variables.
They allow you to combine, compare, and manipulate data.

Python supports a wide range of operators, grouped by the kind of operation they perform.

4.1 Types of Operators

Category Example Operators Description
Assignment =, +=, -= Assign values to variables
Arithmetic +, -, *, /, %, **, // Perform mathematical operations
Comparison ==, !=, >, <, >=, <= Compare values
Logical and, or, not Combine boolean expressions
Bitwise &, |, ^, ~, <<, >> Work with binary (bit-level) data
Identity is, is not Check if two objects are the same
Membership in, not in Check if a value exists in a collection

4.2 Assignment Operators

The assignment operator (=) is used to assign a value to a variable:

Show/Hide Code
age = 7

You can also assign one variable’s value to another:

Show/Hide Code
age = 7
another_variable = age

Since Python 3.8, the walrus operator := allows assignment inside an expression:

Show/Hide Code
if (n := 5) > 3:
  print("n is greater than 3")
n is greater than 3

4.3 Arithmetic Operators

Python includes several standard arithmetic operators:

Operator Name Example Result
+ Addition 2 + 3 5
- Subtraction 5 - 2 3
* Multiplication 4 * 3 12
/ Division 8 / 2 4.0
% Modulus (remainder) 10 % 3 1
** Exponentiation 2 ** 3 8
// Floor Division 7 // 2 3

Examples

Show/Hide Code
print(1 + 1)
print(4 / 2)
print(4 % 3)
print(4 ** 2) 
print(5 // 2)
2
2.0
1
16
2
Note

/ always returns a float, even if the division is even.

Unary Operators

You can use - as a unary minus to change the sign of a number:

Show/Hide Code
print(-4)   # -4
-4

And + can also be used with strings for concatenation:

Show/Hide Code
print("Pazzo" + " is a good dog")
Pazzo is a good dog

4.4 7.4 Compound Assignment Operators

Python lets you combine arithmetic with assignment in a single step:

Operator Meaning Example Equivalent To
+= Add and assign x += 2 x = x + 2
-= Subtract and assign x -= 2 x = x - 2
*= Multiply and assign x *= 3 x = x * 3
/= Divide and assign x /= 2 x = x / 2
%= Modulus and assign x %= 3 x = x % 3

Example:

Show/Hide Code
age = 7
age += 1
print(age)
8

4.5 Comparison Operators

Comparison operators are used to compare two values.
They always return a Boolean result (True or False).

Operator Meaning Example Result
== Equal to 3 == 3 True
!= Not equal to 3 != 2 True
> Greater than 4 > 2 True
< Less than 4 < 2 False
>= Greater or equal 4 >= 4 True
<= Less or equal 2 <= 3 True

Example:

Show/Hide Code
a = 1
b = 2

print(a == b)
print(a != b)
print(a > b)
print(a <= b)
False
True
False
True

4.6 Logical Operators (Boolean)

Logical operators are used to combine or modify Boolean expressions.

Operator Meaning Example Result
not Logical NOT not True False
and Logical AND True and False False
or Logical OR True or False True

Example:

Show/Hide Code
condition1 = True
condition2 = False

print(not condition1)
print(condition1 and condition2)
print(condition1 or condition2)
False
False
True

Truthy and Falsy Behavior

Python treats some non-Boolean values as “truthy” or “falsy”.

Falsy values include:
False, 0, '', [], {}, Non e

That’s why:

Show/Hide Code
print(0 or 1)
print(False or 'hey')
print('hi' or 'hey')
print([] or False)
1
hey
hi
False
  • or returns the first truthy value, or the last one if all are falsy.

  • and returns the first falsy value, or the last one if all are truthy.

Show/Hide Code
print(0 and 1)
print(1 and 0) 
print('hi' and 'hey')
0
0
hey

4.7 Bitwise Operators

Bitwise operators work at the binary (bit) level.
They are mainly used in low-level programming, encryption, and performance optimization.

Operator Name Description
& AND Sets each bit to 1 if both bits are 1
| OR Sets each bit to 1 if at least one bit is 1
^ XOR Sets each bit to 1 if only one of the bits is 1
~ NOT Inverts all bits (0 → 1, 1 → 0)
<< Shift Left Shifts bits to the left, filling with 0s on the right
>> Shift Right Shifts bits to the right, discarding bits on the right
Show/Hide Code
a = 5   # 101 in binary
b = 3   # 011 in binary

print(a & b)  # 1  (001)
print(a | b)  # 7  (111)
print(a ^ b)  # 6  (110)
print(~a)     # -6
print(a << 1) # 10
print(a >> 1) # 2
1
7
6
-6
10
2

4.8 Identity and Membership Operators

Identity Operators

Used to check if two objects are actually the same (not just equal in value).

Show/Hide Code
a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(a is b)      # True  (same object in memory)
print(a is c)      # False (different objects)
print(a == c)      # True  (same contents)
True
False
True

Membership Operators

Used to test whether a value is part of a sequence (like list, string, or tuple).

Show/Hide Code
numbers = [1, 2, 3, 4, 5]

print(3 in numbers)
print(6 not in numbers)
True
True
📚Try it
  1. Create two variables, x = 5 and y = 10.

  2. Use arithmetic, comparison, and logical operators to:

  • Check if y is greater than x

  • Add 5 to x

  • Check if both are even numbers

  1. Print all results
Show/Hide Code
x = 5
y = 10

print(y > x)
x += 5
print(x)
print(x % 2 == 0 and y % 2 == 0)
True
10
True

5 The Ternary Operator in Python

The ternary operator in Python allows you to write conditional expressions in a single line.
It’s a compact way of saying “if this condition is true, do this; otherwise, do that.

5.1 The Standard Way (Using if and else)

Let’s start with a regular if statement:

Show/Hide Code
def is_adult(age):
    if age >= 18:
        return True
    else:
        return False

This works perfectly fine, but it takes four lines of code. Python provides a shorter way to express the same logic using a ternary operator.

5.2 The Ternary Operator Syntax

The syntax of the ternary operator is:

Show/Hide Code
# <value_if_true> if <condition> else <value_if_false>

This can be read as:

“Return <value_if_true> if <condition> is true, otherwise return <value_if_false>.”

Show/Hide Code
def is_adult(age):
    return True if age >= 18 else False

Or even shorter:

Show/Hide Code
status = "Adult" if age >= 18 else "Minor"
  • If age is 20, status will be "Adult".

  • If age is 15, status will be "Minor".

Visual Flow of a Ternary Expression

Here’s how the ternary logic flows:

```{sql connection=, eval: false} ┌───────────────┐ │ Condition? │ └──────┬────────┘ │ ┌──────────┴──────────┐ │ │ True │ │ False ▼ ▼




Example:

::: {#5bdb676f .cell execution_count=56}
``` {.python .cell-code}
"Adult" if age >= 18 else "Minor"
'Minor'

:::

If the condition age >= 18 is True, the result is "Adult". Otherwise, it’s "Minor".

5.3 Why Use the Ternary Operator?

Advantages:

  • Makes your code more concise and readable for simple conditions.

  • Reduces the number of lines.

  • Perfect for inline decisions.

Avoid using it for complex conditions, as it can hurt readability.
If your condition or expressions are long, it’s better to use a normal if...else block.

Nested Ternary Operators (Use With Care)

You can nest ternary operators, but it quickly becomes hard to read:

Show/Hide Code
result = "Positive" if x > 0 else "Zero" if x == 0 else "Negative"

This works, but for clarity, you should use standard if...elif...else for more than one condition.

Example:

Show/Hide Code
temperature = 30
message = "Hot" if temperature > 25 else "Cold"
print(message)
Hot

6 Strings in Python

What is a String

A string in Python is a sequence of characters enclosed in single quotes (' ') or double quotes (" ").

Show/Hide Code
"Prosper"
'Arsenal'
'Arsenal'

You can assign a string value to a variable:

Show/Hide Code
name = "Arsenal"

Strings are one of the most common data types in Python — used to represent names, text, sentences, or any character-based data.

String Concatenation

You can combine (concatenate) two or more strings using the + operator:

Show/Hide Code
phrase = "Arsenal" + " is a good team"
print(phrase)
Arsenal is a good team

You can also append to an existing string using +=:

Show/Hide Code
name = "Manchester United"
name += " is a bad team"
print(name)
Manchester United is a bad team

Converting Numbers to Strings

To concatenate strings with numbers, convert the number using str():

Show/Hide Code
print("Arsenal has won " + str(13) + " Premier League titles")
Arsenal has won 13 Premier League titles

If you try to concatenate a number without converting it, Python will raise a TypeError.

Multi-line Strings

Strings can span multiple lines when enclosed in triple quotes (''' or """):

Show/Hide Code
print("""FC Barcelona is 

    125

years old
""")
FC Barcelona is 

    125

years old

Both single and double triple quotes work the same way:

Show/Hide Code
print('''Mbappe is
26 years old
''')
Mbappe is
26 years old

6.1 String Methods

Python provides many built-in string methods that let you manipulate and analyze text easily.

Method Description Example
isalpha() Checks if string has only letters "abc".isalpha()True
isalnum() Checks if string has letters and/or numbers "abc123".isalnum()True
isdecimal() Checks if string has only digits "123".isdecimal()True
lower() Converts to lowercase "ROGER".lower()"roger"
upper() Converts to uppercase "roger".upper()"ROGER"
title() Capitalizes first letters "roger dog".title()"Roger Dog"
startswith() Checks if string starts with substring "Roger".startswith("Ro")True
endswith() Checks if string ends with substring "Roger".endswith("er")True
replace() Replaces part of a string "Roger".replace("Ro", "Do")"Doger"
split() Splits string by separator "a,b,c".split(",")["a","b","c"]
strip() Removes whitespace from both ends " Roger ".strip()"Roger"
join() Joins elements into one string ",".join(["a","b"])"a,b"
find() Finds position of substring "Roger".find("g")2
Note

All string methods return a new string, they don’t modify the original one.

Global String Functions

The built-in len() function returns the length of a string:

Show/Hide Code
team = "Arsenal"
print(len(team))
7

The in operator checks whether a substring exists within another string:

Show/Hide Code
fact = "Lionel Messi is the GOAT of football"
print("Messi" in fact)
True

6.2 Escaping Characters

To include special characters inside strings, use the backslash (\) escape character.

Escape Sequence Meaning Example
\" Double quote "He said \"Hello\""He said "Hello"
\' Single quote 'It\'s fine'It's fine
\\ Backslash "C:\\Users"C:\Users
\n New line "Hello\nWorld" → prints on two lines
\t Tab "Hello\tWorld" → adds a tab space

Example:

Show/Hide Code
quote = "He said \"Ronaldo is great!\""
print(quote)
He said "Ronaldo is great!"

Indexing and Slicing Strings

Each character in a string has an index number, starting from 0.

Show/Hide Code
name = "Liverpool"
print(name[0])
print(name[1])
print(name[2])
L
i
v

Negative indices count from the end:

Show/Hide Code
print(name[-1])
print(name[-2])
l
o

You can also slice strings to extract a range of characters:

Show/Hide Code
name = "Prosper"
print(name[0:3])
print(name[:2])
print(name[2:])
Pro
Pr
osper

7 Booleans in Python

A Boolean represents one of two values:

  • True

  • False

These are capitalized in Python (not true or false like in some other languages).

Booleans are part of the built-in bool data type, used to express logical truth or falsehood in your programs.

Why Booleans Matter

Booleans are essential in decision-making, they control how your program behaves under different conditions.
They’re especially useful with conditional statements like if, else, and while .

Example:

Show/Hide Code
done = True

if done:
    print("Task completed!")
else:
    print("Task not done yet.")
Task completed!

7.1 Boolean Values from Other Types

Python automatically evaluates many types of values as either True or False when used in a Boolean context (like if statements).

Data Type Example Value Evaluates To
Numbers 0 False
Numbers 1, -5, 3.14 True
Strings "" (empty string) False
Strings "Hello" True
Lists / Tuples / Sets / Dicts [], (), {}, dict() (empty) False
Lists / Tuples / Sets / Dicts Non-empty ([1], {"a"}, {"key": 1}) True
None None False

Example:

Show/Hide Code
print(bool(0))
print(bool(42))
print(bool(""))
print(bool("Python"))
print(bool([]))
print(bool([1, 2, 3]))
False
True
False
True
False
True

Checking if a Value is Boolean

You can verify whether a variable is a Boolean using either type() or isinstance().

Show/Hide Code
done = True

print(type(done) == bool)
print(isinstance(done, bool))
True
True

The bool() Constructor

You can convert other data types into booleans using the bool() function.

Show/Hide Code
print(bool(1))
print(bool(0))
print(bool("hello"))
print(bool("")) 
True
False
True
False

This is often used to simplify conditions or clean data for logical evaluation.

7.2 Combining Booleans with Logical Operators

Booleans work closely with logical operators:

Operator Meaning Example Result
and True if both are True True and False False
or True if at least one is True True or False True
not Negates the boolean not True False

Example:

Show/Hide Code
is_sunny = True
is_weekend = False

print(is_sunny and is_weekend)
print(is_sunny or is_weekend)
print(not is_weekend)
False
True
True

Boolean Logic Diagram

The any() and all() Functions

Python provides two very useful built-in functions for working with Boolean lists or iterables:

any()

Returns True if at least one element in the list is True.

Show/Hide Code
book_1_read = True
book_2_read = False

read_any_book = any([book_1_read, book_2_read])
print(read_any_book)
True

all()

Returns True only if all elements in the list are True.

Show/Hide Code
book_1_read = True
book_2_read = False

read_any_book = all([book_1_read, book_2_read])
print(read_any_book)
False
Function Returns True if… Example Result
any() At least one value is True any([False, True, False]) True
all() All values are True all([True, True, True]) True

7.3 Booleans in Action (Mini Example)

Let’s combine what we learned into a simple example:

Show/Hide Code
user_logged_in = True
has_admin_rights = False

if user_logged_in and has_admin_rights:
    print("Welcome, Admin!")
elif user_logged_in and not has_admin_rights:
    print("Welcome, User!")
else:
    print("Please log in.")
Welcome, User!

8 Numbers in Python

Numbers are one of the most fundamental data types in Python.
They can be of three main types:

Type Class Example Description
Integer int 8 Whole numbers, positive or negative, without decimals.
Floating Point float 3.14 Real numbers with decimal points.
Complex complex 2 + 3j Numbers with real and imaginary parts.

Integer Numbers in Python

Integer numbers are represented using the int class.

Example:

Show/Hide Code
age = 8
print(type(age))
<class 'int'>

You can also use the constructor:

Show/Hide Code
age = int(8)

Floating Point Numbers in Python

Floating point numbers (fractions or decimals) use the float class.

Show/Hide Code
fraction = 0.1
print(type(fraction))
<class 'float'>

You can also define them with the constructor:

Show/Hide Code
fraction = float(0.1)

Complex Numbers in Python

Complex numbers are represented using the complex class.

Show/Hide Code
complexNumber = 2 + 3j
print(type(complexNumber))
<class 'complex'>

Or you can create them using the constructor:

Show/Hide Code
complexNumber = complex(2, 3)

You can access their real and imaginary parts:

Show/Hide Code
complexNumber.real
complexNumber.imag
3.0

Arithmetic Operations on Numbers

You can perform arithmetic operations using Python’s arithmetic operators:

Operator Name Example Result
+ Addition 1 + 1 2
- Subtraction 2 - 1 1
* Multiplication 2 * 2 4
/ Division 4 / 2 2.0
% Modulus (Remainder) 4 % 3 1
** Exponentiation 4 ** 2 16
// Floor Division 7 // 2 3

Compound Assignment Operators

These let you perform an operation and assignment in one step:

Operator Example Equivalent To
+= x += 1 x = x + 1
-= x -= 1 x = x - 1
*= x *= 2 x = x * 2
/= x /= 3 x = x / 3
%= x %= 2 x = x % 2

Example:

Show/Hide Code
age = 7
age += 1
age
8

Built-in Numeric Functions

Python provides several built-in functions for numeric manipulation:

Function Description Example Output
abs(x) Absolute value abs(-7) 7
round(x) Round to nearest integer round(4.6) 5
round(x, n) Round to n decimals round(3.14159, 2) 3.14

Useful Math Modules

Python’s standard library provides several modules for mathematical operations:

Module Purpose
math Common mathematical functions and constants (e.g., sqrt, pi, sin)
cmath Functions for complex numbers
decimal High-precision decimal arithmetic
fractions Operations on rational numbers

Example:

Show/Hide Code
import math
print(math.sqrt(16))
print(math.pi)
4.0
3.141592653589793

8.1 Using Math, Decimal, and Fractions Modules in Python

  1. The math Module

The math module provides access to common mathematical functions and constants.

Show/Hide Code
import math

print(math.pi)
print(math.sqrt(16))
print(math.factorial(5))
print(math.ceil(2.3))
print(math.floor(2.9))
3.141592653589793
4.0
120
3
2

Useful constants and functions

  • math.pi → π (pi)

  • math.e → Euler’s number (≈2.718)

  • math.pow(x, y) → x raised to power y

  • math.log(x, base) → logarithm

  • math.sin(), math.cos(), math.tan() → trigonometric functions

  1. The decimal Module

The decimal module helps you perform high-precision decimal arithmetic, useful in finance or where rounding errors are unacceptable.

Show/Hide Code
from decimal import Decimal, getcontext

getcontext().prec = 4  # set precision to 4 decimal places
x = Decimal('1.10')
y = Decimal('2.30')
print(x + y)  # precise result
3.40

Why use Decimal? Because regular floats can introduce tiny rounding errors

  1. The fractions Module

The fractions module allows you to represent and work with rational numbers (fractions).

Show/Hide Code
from fractions import Fraction

f1 = Fraction(1, 3)
f2 = Fraction(2, 5)
result = f1 + f2
print(result)
11/15

Fraction can also convert floats or strings:

Show/Hide Code
print(Fraction('0.25'))
print(Fraction(0.5))
1/4
1/2

9 Constants in Python

In Python, constants are variables whose values are meant to stay unchanged throughout the program.
However, unlike other languages (like Java or C++), Python doesn’t have a built-in way to enforce immutability, it relies on naming conventions and discipline.

Declaring Constants (By Convention)

By convention, constants are written in ALL UPPERCASE letters with underscores separating words.

Show/Hide Code
WIDTH = 1024
HEIGHT = 256
APP_NAME = "MyPythonApp"

This tells other developers:

“These values are fixed. Don’t modify them!”

However, Python won’t stop you from reassigning them:

Show/Hide Code
WIDTH = 800  # This works, but it's bad practice!

Using Enums as True Constants

If you want to make your constants truly unchangeable, you can use the Enum class from the enum module.

Show/Hide Code
from enum import Enum

class Constants(Enum):
    WIDTH = 1024
    HEIGHT = 256

Now, you can access constants safely:

Show/Hide Code
print(Constants.WIDTH.value)
print(Constants.HEIGHT.value)
1024
256

Enums prevent accidental reassignment:

Show/Hide Code
# Constants.WIDTH = 500  # ❌ This will raise an error

10 Enums in Python

Enums (short for Enumerations) are a set of symbolic names bound to unique, constant values.
They make your code more readable, organized, and error-resistant .

Defining an Enum

You define an Enum by importing the Enum class and creating a subclass:

Show/Hide Code
from enum import Enum

class State(Enum):
    INACTIVE = 0
    ACTIVE = 1

Now, you can use them like named constants:

Show/Hide Code
print(State.INACTIVE)
print(State.ACTIVE)
State.INACTIVE
State.ACTIVE

Accessing Enum Values

  • Get the name
Show/Hide Code
print(State.ACTIVE.name)
ACTIVE
  • Get the value:
Show/Hide Code
print(State.ACTIVE.value)
1
  • Access by value or name:
Show/Hide Code
print(State(1))
print(State['ACTIVE'])
State.ACTIVE
State.ACTIVE

Iterating and Counting Enum Members

You can list all members:

Show/Hide Code
list(State)
[<State.INACTIVE: 0>, <State.ACTIVE: 1>]

Count how many members exist:

Show/Hide Code
len(State)
2

Loop through members:

Show/Hide Code
for state in State:
    print(state.name, "=", state.value)
INACTIVE = 0
ACTIVE = 1

When to Use Enums

Use Enums when you need:

  • Named constants with clear meaning

  • Predefined states (e.g., ON/OFF, SUCCESS/FAILURE)

  • Safety against accidental changes

11 User Input in Python

Interacting with the user is one of the most common tasks in programming. In Python, you can both display output and receive input from the user using simple built-in functions.

Displaying Output with print()

To show text or data on the screen, we use the print() function.

Show/Hide Code
name = "Bukayo Saka"
print(name)
Bukayo Saka

You can print multiple values by separating them with commas:

Show/Hide Code
print(name, "plays for Arsenal")
print("I like", name)
Bukayo Saka plays for Arsenal
I like Bukayo Saka

[critical] Getting User Input with input()

To let users type something into your program, use the input() function.

Show/Hide Code
print("What is your age?")
age = input()
11
print("Your age is " + age)

How it works:

  • The program pauses and waits for the user to type something.

  • When the user presses Enter, the input is stored in the variable (age here).

  • Everything you get from input() is a string, even numbers.

Converting Input to Numbers

Since input() always returns a string, you often need to convert the input into another type, such as int or float.

Show/Hide Code
age = int(input("Enter your age:"))
print("In 10 years, you will be", age + 10)

Output example:

Show/Hide Code
Enter your age: 25
In 10 years, you will be 35

If you expect decimals:

Show/Hide Code
height = float(input("Enter your height in meters: "))
print("Your height is", height, "m")

Handling Invalid Input

If the user types something that can’t be converted to a number, the program will raise an error.
To prevent this, use a try/except block.

Show/Hide Code
try:
    age = int(input("Enter your age: "))
    print("You are", age, "years old.")
except ValueError:
    print("That doesn’t look like a valid number!")

Combining Output and Input

You can make your input prompts shorter and cleaner by passing the message directly to input():

Show/Hide Code
name = input("What is your name? ")
Prosper
print("Hello,", name + "!")

Output:

Show/Hide Code
What is your name? Prosper
Hello, Prosper!

12 Control Statements in Python

Control statements allow your program to make decisions, running certain code only when a condition is met.
They form the foundation of logic in Python programming.

The if Statement

The simplest form of a control statement is the if statement.
It checks a condition, if it’s True, Python executes the indented block of code that follows.

Show/Hide Code
condition = True

if condition == True:
    print("The condition")
    print("was True")
The condition
was True

How it works:

  • If the condition evaluates to True, the indented code runs.

  • If it’s False, Python skips that block.

What Is a Block?

A block is a group of indented lines of code that belong together.
Indentation (4 spaces by convention) tells Python which statements are inside a block.

Show/Hide Code
if True:
    print("Inside the block")
print("Outside the block")
Inside the block
Outside the block

⚠️ If indentation is inconsistent, Python raises an IndentationError.

The else Statement

You can attach an else clause to handle the case when the condition is False.

Show/Hide Code
condition = False

if condition:
    print("Condition is True")
else:
    print("Condition is False")
Condition is False

The elif Statement

When you have multiple possible conditions, use elif (short for “else if”).

Show/Hide Code
name = "Prosper"

if name == "Beko":
    print("Hello Beko")
elif name == "Prosper":
    print("Hello Prosper")
elif name == "Patrick":
    print("Hello Patrijk")
else:
    print("Hello Stranger")
Hello Prosper

Only one block runs, the first one with a True condition. Once Python finds a match, it skips the rest.

Chaining Multiple Conditions

You can check several conditions in one expression using logical operators (and, or, not):

Show/Hide Code
age = 19
has_id = True

if age >= 18 and has_id:
  print("You are allowed to enter.")
else:
  print("Access denied.")
You are allowed to enter.

Inline if (Ternary Expression)

Sometimes you want to assign a value depending on a condition, all in one line.
That’s when the inline if (also called the ternary operator) is used.

Show/Hide Code
a = 2
result = 2 if a == 0 else 3
print(result)
3

syntax reminder:

Show/Hide Code
<value_if_true> if <condition> else <value_if_false>

Example: Nested Conditions

You can also put one if inside another. This is known as nested if statements.

Show/Hide Code
age = 20
citizen = True

if age >= 18:
    if citizen:
        print("You can vote.")
    else:
        print("You must be a citizen to vote.")
else:
    print("You are too young to vote.")
You can vote.

Visual Flow of if–elif–else

12.1 Summary Table

Keyword Purpose Example
if Executes code if condition is True if x > 0:
elif Checks another condition if previous is False elif x == 0:
else Runs if all previous conditions are False else:
and, or, not Combine or invert conditions if x > 0 and y > 0:
📚 Try It Yourself: Practice Exercises

Try 1: Even or Odd

Write a Python program that asks the user to enter a number and prints whether it is even or odd.

Try 2: Temperature Classifier

Create a program that classifies a temperature input by the user.

  • Below 20°C → “Cold”

  • Between 20°C and 30°C → “Warm”

  • Above 30°C → “Hot”

Try 3: Age Group Identifier

Ask the user for their age and print:

  • “Child” if age < 13

  • “Teenager” if 13 ≤ age < 20

  • “Adult” if 20 ≤ age < 60

  • “Senior” otherwise

13 Lists in Python

Lists are one of the most commonly used data structures in Python.
They allow you to store multiple values in a single variable and access them easily.

Creating Lists

A list is defined using square brackets []:

Show/Hide Code
dogs = ["Max", "Bob"]

A list can hold different data types:

Show/Hide Code
items = ["Cup", 1, "Tree", True]

You can create an empty list:

Show/Hide Code
# items = []

Checking if an Item Exists

Use the in operator:

Show/Hide Code
print("Tree" in items)
print("Peter" in dogs)
True
False

Accessing Items (Indexing)

List indexes start at 0:

Show/Hide Code
items[0] 
items[1]
items[3]
True

Negative Indexing

Negative indexes count from the end:

Show/Hide Code
items[-1]
items[-2]
'Tree'

Changing Items

You can replace values at a specific index:

Show/Hide Code
items[0] = "Banana"

Finding the Index of an Item

Use the index() method:

Show/Hide Code
names = ["Ronaldo", "Messi", "Bukayo saka"]
print(names.index("Messi"))
1
Important

You wrote this incorrectly earlier:
items.index(0) does NOT return "Roger".
index() receives a value, not an index.

List Slicing

You can extract parts of a list:

Show/Hide Code
items[0:2]
items[2:]
items[:3]
['Banana', 1, 'Tree']

Getting the Length of a List

Use len():

Show/Hide Code
len(items)
4

Adding Items

append() → adds ONE item at the end

Show/Hide Code
items.append("Test")

extend() → adds MULTIPLE items

Show/Hide Code
items.extend(["A", "B"])

+= operator → also extends

Show/Hide Code
items += ["C", "D"]
⚠️ Be careful
Show/Hide Code
items += "Test"

will add each character separately.

Removing Items

Use remove():

Show/Hide Code
items.remove("Test") #Removes the first occurrence only.

Inserting Items

Insert one item at a specific index:

Show/Hide Code
items.insert(1, "Test")  
# (value, position)

(The earlier version had arguments reversed — now fixed.)

Insert multiple items at a specific position:

Show/Hide Code
items[1:1] = ["Test1", "Test2"]

Sorting Lists

Basic sorting:

Show/Hide Code
items.sort()
Please note

Sorting only works if items are comparable (e.g., all strings, or all numbers).

Fix inconsistent string cases:

Show/Hide Code
items.sort(key=str.lower)

Copy a list before sorting:

Show/Hide Code
copy_items = items[:]
copy_items.sort()

Or use sorted() (does NOT change original):

Show/Hide Code
new_list = sorted(items, key=str.lower)

Operations Summary Table

Operation Description Example
Create list Define new list items = [1,2,3]
Check item Membership test "a" in items
Access item Get value by index items[0]
Modify item Change a value items[1] = 10
Find index Locate item items.index("Bob")
Slice Extract part items[1:3]
Length Number of items len(items)
Append Add one items.append(x)
Extend Add many items.extend([...])
Insert Add at position items.insert(1, x)
Remove Remove item items.remove(x)
Sort Order list items.sort()

List Index Visualization

14 Tuples in Python

Tuples are another core data structure in Python, very similar to lists, with one important difference:

Tuples are immutable

Once a tuple is created:

  • You cannot add new items

  • You cannot remove items

  • You cannot change existing items

Because of this, tuples are often used for:

  • Fixed collections of values

  • Values that must not be changed

  • Faster operations compared to lists (tuples are more lightweight)

Creating Tuples

Tuples use parentheses ( ) instead of square brackets:

Show/Hide Code
names = ("Prosper", "Kigali")

You can also mix types:

Show/Hide Code
info = ("Prosper", 42, True)

Creating an empty tuple:

Show/Hide Code
empty_tuple = ()

Creating a single-item tuple

Important: you must include a comma, otherwise Python sees it as a normal value.

Show/Hide Code
single = ("Prosper",)  # This is a tuple
not_tuple = ("Prosper")  # This is just a string

Accessing Tuple Elements (Indexing)

Tuples are ordered, so you can access items by index:

Show/Hide Code
names = ("Prosper", "Kigali")
names[0]
names[1]
'Kigali'

Negative indexing works too:

Show/Hide Code
names[-1]
names[-2]
'Prosper'

Useful Tuple Methods

index()

Returns the index of the first matching value:

Show/Hide Code
names.index("Prosper")
names.index("Kigali")
1

count()

Counts how many times a value appears:

Show/Hide Code
("a", "b", "a").count("a")
2

Tuple Length

Use the len() function:

Show/Hide Code
len(names)
2

Membership Test

Check if an item exists with in:

Show/Hide Code
"Roger" in names
"Syd" in names
"Tina" in names
False

Tuple Slicing

Just like lists and strings:

Show/Hide Code
names = ("Messi", "Kigali", "Venice", "Nyamirambo")

names[0:2]
names[1:]
names[:3]
('Messi', 'Kigali', 'Venice')
Note

slicing returns a new tuple, since tuples are immutable.

Sorting Tuples

sorted() can sort the tuple but returns a list, not a tuple:

Show/Hide Code
sorted(names) 
['Kigali', 'Messi', 'Nyamirambo', 'Venice']

Convert back to a tuple if needed:

Show/Hide Code
tuple(sorted(names))
('Kigali', 'Messi', 'Nyamirambo', 'Venice')

Concatenating Tuples

Use the + operator:

Show/Hide Code
names = ("Rutsiro", "Karongi")
new_tuple = names + ("Rubavu", "Musanze")

print(new_tuple)
('Rutsiro', 'Karongi', 'Rubavu', 'Musanze')

Why Use Tuples Instead of Lists?

Tuples are chosen when:

  • The data must remain unchanged

  • The collection is fixed

  • You want faster performance

  • You want to use the values as dictionary keys or set elements (only immutable objects can be used as keys)

Example:

Show/Hide Code
location = (10, 20)
positions = {location: "Player1"}  # Works because tuple is hashable
location
positions
{(10, 20): 'Player1'}

Lists cannot do this because they are mutable.

When to Use Tuples (Examples)

Returning multiple values from a function:

Show/Hide Code
def get_user():
    return ("Prosper", 42)

name, age = get_user()

Representing fixed coordinates:

Show/Hide Code
point = (12.5, 4.8)

Keeping configuration constants:

Show/Hide Code
ALLOWED_ROLES = ("admin", "editor", "viewer")

Tuple indexing works the same way as list indexing.

15 Dictionaries in Python

Dictionaries are one of the most powerful and widely used data structures in Python.
While lists store collections of values, dictionaries store collections of key–value pairs, allowing for fast lookups, updates, and flexible data organization.

15.1 Creating Dictionaries

You can create a dictionary using curly braces {} with key–value pairs:

Show/Hide Code
artist = {'name': 'Kivumbi'}

A dictionary can contain multiple key–value pairs:

Show/Hide Code
artist = {'name': 'Kivumbi', 'age': 26}

Keys must be immutable

Common key types include:

  • strings

  • numbers

  • tuples

Values can be of any type (strings, numbers, lists, other dictionaries, etc.)

15.2 Accessing Dictionary Values

Access a value by using its key:

Show/Hide Code
artist['name']
artist['age']
26

If you try to access a key that doesn’t exist, Python raises a KeyError.

15.3 Updating Values

You can modify a value using the same key-access notation:

Show/Hide Code
artist['name'] = 'Bwiza'

15.4 Using get()

get() allows safe access to keys and lets you specify a default value:

Show/Hide Code
artist.get('name')
artist.get('color', 'N/A')
'N/A'

15.5 Removing Items

pop()

Removes a key and returns its value:

Show/Hide Code
artist.pop('name')
'Bwiza'

popitem()

Removes and returns the last inserted key–value pair:

Show/Hide Code
artist.popitem()
('age', 26)

15.6 Checking for Keys

Use the in operator:

Show/Hide Code
player = {'name': 'Lamine', 'age': 18} 
'name' in player
True

15.7 Getting Keys, Values, and Items

Convert the dictionary views to lists if needed:

Show/Hide Code
list(player.keys())
list(player.values())
list(player.items())
[('name', 'Lamine'), ('age', 18)]

items() returns a list of (key, value) tuples.

Dictionary Length

Show/Hide Code
len(player)
2

15.8 Adding Items

Add a new key–value pair simply by assignment:

Show/Hide Code
player['sport'] = 'Football'

Removing Items with del

Show/Hide Code
del player['sport']

Copying Dictionaries

Use the copy() method to create a shallow copy:

Show/Hide Code
playerCopy = player.copy()

Key–Value Mapping

Try it

Try 1: Basic Dictionary Operations

  1. Create a dictionary student with keys: 'name', 'age', and 'courses' (a list of course names).

  2. Print the 'name' and 'age' values.

  3. Add a new key 'grade' with value 'A'.

  4. Remove the 'age' key from the dictionary.

  5. Use get() to try to get the value of 'address', but if it doesn’t exist, return 'Unknown'.

  6. Print the full dictionary at the end

Try 2: Dictionary Manipulation & Summary

You are given the following dictionary of product quantities:

Show/Hide Code
inventory = {
    'apples': 30,
    'bananas': 18,
    'oranges': 25
}

Perform the following tasks:

  1. Add a new product 'pears' with quantity 12.

  2. Update the quantity of 'bananas' to 30.

  3. Use items() to print all product–quantity pairs, one per line.

  4. Calculate and print the total quantity of all products.

  5. Check if 'grapes' exists as a key in the inventory; if not, print a message: “Grapes are not in inventory.”

16 Sets in Python

Sets are another key data structure built into Python.
They behave like mathematical sets: unordered collections of unique elements, optimized for membership tests, deduplication, and “set arithmetic”.

According to the official Python:

“A set is an unordered collection with no duplicate elements.”
Sets support operations like union, intersection, difference and symmetric difference.

What-makes a Set Special?

  • Unordered: no index or guaranteed order.

  • Unique elements only: duplicates are automatically removed.

  • Mutable (for standard set): you can add or remove elements.

  • There is an immutable version, frozenset, which cannot be changed after creation.

Creating Sets

Literal syntax:

Show/Hide Code
names = {"Prosper", "Kigali"}

Set constructor:

Show/Hide Code
names = set(["Prosper", "Kigali"])
Note

Creating an empty set

You cannot use {} (that creates an empty dictionary). Use set() instead:

Show/Hide Code
empty = set()

16.1 Basic Set Operations

Membership test:

Show/Hide Code
"Prosper" in names
"Rusizi" in names
False

Length of a set:

Show/Hide Code
len(names) 
2

Convert to list (for ordered processing):

16.2 Mathematical Set Operations

Operation Example Code Result
Intersection (common elements) set1 & set2 or set1.intersection(set2) elements in both
Union (all elements) `set1 set2orset1.union(set2)`
Difference (items in first not in second) set1 - set2 or set1.difference(set2) elements only in first
Symmetric Difference (in either but not both) set1 ^ set2 or set1.symmetric_difference(set2) elements exclusive to each

Examples:

Show/Hide Code
set1 = {"Messi", "Dimaria"}
set2 = {"Cristiano", "Dimaria"}

intersect  = set1 & set2
union      = set1 | set2
difference = set1 - set2
sym_diff   = set1 ^ set2
print(intersect)
print(union)
print(difference)
print(sym_diff)
{'Dimaria'}
{'Cristiano', 'Dimaria', 'Messi'}
{'Messi'}
{'Cristiano', 'Messi'}

From documentation:

“Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.”

16.3 Subset / Superset Checks

Show/Hide Code
set1 = {"Mugisha", "Benjamin"}
set2 = {"Benjamin"}

set2 <= set1  # (set2 is subset of set1)
set1 >= set2  # (set1 is superset of set2)
True

One can also use <, > for proper subset/superset (when not equal).

16.4 Adding and Removing Items

Add an element:

Show/Hide Code
names.add("Musanze")

Remove an element:

Show/Hide Code
# names.remove("Huye")    # raises KeyError if missing
names.discard("Huye")   # safer — no error if missing

Pop an arbitrary element:

Show/Hide Code
item = names.pop()    # removes and returns some element (no order)

Clear all elements:

Show/Hide Code
names.clear()

16.5 Immutability: frozenset

When you need a hashable, immutable set (for using as dictionary keys, set elements, etc.), use frozenset:

Show/Hide Code
fs = frozenset(["Roger", "Syd"])
# fs.add("Luna") → TypeError: cannot modify frozenset
Try it

Try 1: Unique Words from a Sentence

  1. Ask the user to input a sentence (via input()).

  2. Split the sentence into words and store them in a set (so duplicates are removed).

  3. Print how many unique words there are.

  4. Print the set of unique words.

Try 2: Set Operations on Skill Sets You have two sets of skills:

You have two sets of skills:

Show/Hide Code
skills_A = {"Python", "R", "SQL"}
skills_B = {"Java", "Git", "Python", "SQL"}

Tasks:

  1. Compute and print the union of the two sets (all skills either A or B has).

  2. Compute and print the intersection (skills both A and B share).

  3. Compute and print the difference of skills_A minus skills_B (skills in A but not in B).

  4. Check if skills_A is a subset of skills_B, and print the result (True/False).

17 Functions in Python

A function in Python is a named block of code that you can define once and run (call) whenever needed. It helps you organize your code, avoid repetition, and make your programs easier to understand and maintain.

Why Functions Matter

  • Functions allow you to break your program into smaller, reusable pieces (“modular code”). datacamp.com

  • They improve readability and help you avoid writing the same code multiple times.

  • A function can take inputs (parameters), execute logic, and optionally return a value.

17.1 Defining and Calling a Function

Definition

Use the def keyword, name your function, possibly specify parameters, then a colon and an indented block of code:

Show/Hide Code
def hello():
    print('Hello!')

Here hello is the function name; the body (print statement) is indented. Without calling it, nothing will happen.

Call

To run the function, use:

Show/Hide Code
hello()
Hello!

You can call the function multiple times.

Parameters and Arguments

  • Parameters are the names listed in the function definition.

  • Arguments are the actual values you pass when calling the function.

Example:

Show/Hide Code
def hello(name):
    print('Hello ' + name + '!')

hello('Munezero')   # 'Munezero' is argument; name is parameter
Hello Munezero!

Default Parameter Values

You can provide default values so the caller doesn’t have to pass that argument:

Show/Hide Code
def hello(name='my friend'):
    print('Hello ' + name + '!')

hello()
hello('Ngarambe')
Hello my friend!
Hello Ngarambe!

Multiple Parameters

Show/Hide Code
def hello(name, age):
    print('Hello ' + name + ', you are ' + str(age) + ' years old!')

hello('Kanamugire', 7)
Hello Kanamugire, you are 7 years old!

17.1.1 Passing Mutable vs Immutable Types

  • Immutable types (int, float, string, tuple) cannot be changed inside the function in a way that impacts the caller.

    Show/Hide Code
    def change(value):
      value = 2
    
    val = 1
    change(val)
    print(val)
  • Mutable types (list, dict, set) can be changed inside the function and those changes will reflect outside.

Returning Values

Functions can use the return statement to send a value back to the caller, and that ends the function execution

Show/Hide Code
def hello(name):
    print('Hello ' + name + '!')
    return name

result = hello('Kanamugire')
print(result)
Hello Kanamugire!
Kanamugire

You can omit a value and just write return (or no return), in which case the function returns None.

Show/Hide Code
def hello(name):
    if not name:
        return
    print('Hello ' + name + '!')

hello('')

You can return multiple values separated by commas, Python packs them into a tuple.

Show/Hide Code
def hello(name):
    print('Hello ' + name + '!')
    return name, 'Prosper', 7

res = hello('Beko')
print(res)   # ('Syd', 'Roger', 8)
Hello Beko!
('Beko', 'Prosper', 7)

Good Function Design & Best Practices

  • Use descriptive function names (verbs): calculate_total(), get_user_input().

  • Keep functions short and focused, they should do one task.

  • Use default values for common cases.

  • Document what the function does (docstrings).

  • Avoid side effects unless intended (modifying global data).

  • Use return values rather than printing inside if you need to reuse results.

Try it

Try 1: Greeting Function
Write a function greet_user() that asks the user for their name, then prints:

hello <name>, welcome!

If the user just presses Enter without typing a name, the function should print:

hello, welcome!

Exercise 2 – Temperature Converter
Write a function convert_temperature(temp, unit) that receives a temperature temp (number) and a unit (‘C’ or ‘F’).

  • If unit is 'C', convert temp to Fahrenheit, print the result.

  • If unit is 'F', convert temp to Celsius, print the result.
    Use the formulas:

    F = C * 9/5 + 32
    C = (F − 32) * 5/9

18 Objects in Python

Python is built on a very important idea:

Everything in Python is an object.

This includes:

  • This includes:

  • Integers (int)

  • Strings (str)

  • Floats (float)

  • Lists (list)

  • Tuples (tuple)

  • Dictionaries (dict)

  • Functions

  • Even modules and classes themselves

Every object has:

  • Attributes → stored data

  • Methods → functions that belong to the object

You access both using dot notation:

Integer Objects

Let’s define a simple integer:

Show/Hide Code
age = 8

Even though 8 looks like a primitive value, it is actually a full Python object. This means it has attributes and methods, just like any other object:

Show/Hide Code
print(age.real)
print(age.imag)
print(age.bit_length())
8
0
4

Explanation:

  • real → real part of the number

  • imag → imaginary part (0 for real numbers)

  • bit_length() → number of bits required to represent the number in binary

List Objects

When you create a list:

Show/Hide Code
items = [1, 2]

The list object exposes methods such as:

Show/Hide Code
items.append(3) # Adds item at the end
items.pop()     # Removes last item
3

Each object type exposes different methods, because different types behave differently.

The id() Function: Memory Address

Python provides the built-in function id() to inspect the memory address of an object:

Show/Hide Code
id(age)
140723354798744

The number will differ on your machine.

Reassigning a Variable Changes Its Object

If you change the value of a variable, Python creates a new object in memory.

Show/Hide Code
age = 8
print(id(age))

age = 9
print(id(age))
140723354798744
140723354798776

The two id() values will be different because:

  • 8 and 9 are different objects

  • Reassigning the variable makes it point to a new object

Modifying a Mutable Object Keeps the Same Address

Lists are mutable, meaning they can be changed in place.

Show/Hide Code
items = [1, 2]
print(id(items))

items.append(3)
print(items)  # [1, 2, 3]
print(id(items))
2069010423168
[1, 2, 3]
2069010423168

The memory address stays the same because the list object itself is modified, not replaced.

Mutable vs Immutable Objects

Python objects come in two main types:

18.0.1 Immutable Objects

Cannot be changed after creation.
Examples:

  • int

  • float

  • bool

  • str

  • tuple

If you “change” them, Python creates a new object.

Example:

Show/Hide Code
age = 8
print(id(age))

age += 1
print(id(age))
140723354798744
140723354798776

The id() value changes → new object.

18.0.2 Mutable Objects

Can change without replacing the object.
Examples:

  • list

  • dict

  • set

Modifying a list:

Show/Hide Code
things = [1, 2]
print(id(things))

things.append(3)
print(id(things))  # Same address
2069010739392
2069010739392
Try it

Q1: What will the output show about memory addresses?

Show/Hide Code
a = 5
b = a
a += 1

print(id(a))
print(id(b))

a) Do a and b point to the same object at the end?
b) Why?

Q2: Identify each object as mutable or immutable:

  1. "hello"

  2. 1, 2, 3]

  3. (10, 20)

  4. { "name": "Ada" }

  5. 7.5

Q3: Given the code below, will the id(items) change after the last line? Why?

Show/Hide Code
items = [10, 20]
items = items + [30]

19 Loops in Python

Loops allow us to repeat a block of code multiple times.
They are one of the most important control structures in programming.

Python provides two types of loops:

  1. while loops → repeat as long as a condition is true

  2. for loops → repeat for each item in a sequence

19.1 while Loops

A while loop runs its block as long as the condition evaluates to True.

Basic Structure

Show/Hide Code
while condition:
    # repeated code
Show/Hide Code
while condition:
    # repeated code

Example: Infinite Loop

Show/Hide Code
#condition = True
#while condition == True:
#    print("The condition is True")

This loop never ends because condition never becomes False.

Stopping a While Loop

We can stop the loop by updating the condition:

Show/Hide Code
condition = True
while condition == True:
    print("The condition is True")
    condition = False

print("After the loop")
The condition is True
After the loop

Flow:

  1. First iteration → condition is True

  2. We print the message

  3. We set condition = False

  4. Next iteration → condition is False → loop stops

Using a Counter

A common pattern uses a counter variable:

Show/Hide Code
count = 0
while count < 10:
    print("Count is:", count)
    count = count + 1

print("Loop finished")
Count is: 0
Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
Count is: 6
Count is: 7
Count is: 8
Count is: 9
Loop finished

how it works:

count = 0

Is count < 10?
↓ Yes
Print count → count += 1

Loop again
↓ No
End loop

Common Mistake: Forgetting Counter Update

This causes an infinite loop:

Show/Hide Code
# count = 0
# while count < 5:
#     print(count)   # Never changes -> infinite loop!

19.2 for Loops in Python

A for loop allows you to iterate over:

  • lists

  • strings

  • tuples

  • ranges

  • dictionaries

  • etc.

Python takes each item in a sequence and assigns it to a loop variable.

Example: Loop Through a List

Show/Hide Code
items = [1, 2, 3, 4]
for item in items:
    print(item)
1
2
3
4

Looping a Set Number of Times Using range()

range(n) generates numbers from 0 to n-1.

example:

Show/Hide Code
for item in range(4):
    print(item)
0
1
2
3

Using enumerate() to Get Index + Value

enumerate() returns both the index and the item:

Show/Hide Code
items = [1, 2, 3, 4]
for index, item in enumerate(items):
    print(index, item)
0 1
1 2
2 3
3 4

19.3 while vs for, When to Use Which

Use Case Best Loop
You know the number of iterations for
You want to repeat until a condition changes while
Iterating over lists/strings for
Waiting for user input while
Building counters both work

For Loop Over a List (illustration)

19.4 Common Errors

Mistake: Misusing range()

Show/Hide Code
range(1,5)   # gives 1,2,3,4 (not 5)
range(1, 5)

Mistake: Changing a list while iterating over it

Mistake: Infinite while loops caused by forgetting updates

Try it

Q 1

Write a while loop that prints numbers 1 to 5.

Q 2

What is the output of :

Show/Hide Code
for i in range(3):
    print("Loop:", i)

Q 3

Using enumerate(), print:

Index 0 -> apple

Index 1 -> banana

Index 2 -> mango

from this list: fruits = [“apple”, “banana”, “mango”]

Q 4

Does the following code create an infinite loop? Why or why not?

Show/Hide Code
x = 0 
while x < 3:
    print(x)

19.5 Break and Continue in Python

Inside both while and for loops, Python provides two special keywords that control how the loop behaves:

1. continue

Skips the current iteration and jumps to the next iteration of the loop.

2. break

Immediately stops the loop completely, and Python continues with the next line after the loop.

These tools give you more control inside loops, especially when you need to skip certain values or stop early.

19.6 The continue Statement

continue tells Python:

“Skip the rest of this loop iteration and move on to the next one.”

Example—Skip number 2:

Show/Hide Code
items = [1, 2, 3, 4]
for item in items:
    if item == 2:
        continue
    print(item)
1
3
4

Explanation:

  • When item == 2, the continue statement triggers

  • Python skips the print() and moves to the next item

19.7 The break Statement

break tells Python:

“Stop the loop right now and jump out completely.”

Example—Stop when item reaches 2:

Show/Hide Code
items = [1, 2, 3, 4]
for item in items:
    if item == 2:
        break
    print(item)
1

19.7.1 Explanation:

  • When item == 2, Python hits break

  • Loop ends immediately

  • It does not check items 3 and 4

  • Execution continues after the loop

19.8 Using break and continue in while Loops

Skip even numbers:

Show/Hide Code
x = 0
while x < 5:
    x += 1
    if x % 2 == 0:
        continue
    print(x)
1
3
5

Stop when x reaches 3:

Show/Hide Code
x = 0
while x < 5:
    x += 1
    if x == 3:
        break
    print(x)
1
2
Try it

Q 1

Write a loop that prints numbers from 1 to 10, but skips 5.

Q 2

Write a loop that prints numbers from 1 to 10, but stops when number is 7.

Q 3

given:

Show/Hide Code
names = ["Messi", "Pele", "Cristiano", "Maradona"]

Write a loop that prints each name except “Cristiano”.

Exercise 4

What does this code print?

Show/Hide Code
for x in range(5):
    if x == 2:
        continue
    if x == 4:
        break
    print(x)
Show/Hide Code
#Explanation:

#x = 0 → print
#x = 1 → print
#x = 2 → continue (skip print)
#x = 3 → print
#x = 4 → break → stop

20 Classes in Python

Object-Oriented Programming (OOP) is a major part of Python.
While Python gives you built-in types like int, str, list, you can also create your own types using classes .

A class is a blueprint.
An object is an instance of that blueprint.

Defining a Class

To define a class, use the class keyword:

Show/Hide Code
class Dog:
    # class definition
    pass

Right now the class does nothing, but it exists as a type.

Creating Objects (Instances)

You create an object by calling the class as if it were a function:

Show/Hide Code
Max = Dog()

Now Max is an object of type Dog.

Show/Hide Code
print(type(Max))
<class '__main__.Dog'>

Adding Methods to a Class

A class normally contains methods (functions inside a class).

Show/Hide Code
class Dog:
    def bark(self):
        print('WOF!')
  • Methods must include self as the first parameter.

  • self refers to the current instance.

Example:

Show/Hide Code
Max = Dog()
Max.bark()
WOF!

What is self?

self is a reference to the object calling the method.

Dog object (Max)

Max.bark()

self = Max inside method

20.1 Initializing Objects With __init__

__init__() is a constructor.
It runs automatically whenever you create an object.

Example:

Show/Hide Code
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print('WOF!')

Creating an object:

Show/Hide Code
Max = Dog("Roger", 8)
print(Max.name)
print(Max.age)

Max.bark()
Roger
8
WOF!

20.1.1 What __init__ does:

  • Assigns values to object attributes

  • Prepares the object for use

20.2 Attributes and Methods

Objects can have:

  • Attributes → Variables that belong to the object

  • Methods → Functions that belong to the object

Example:

Show/Hide Code
class Dog:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(self.name + " says WOF!")

20.3 Class vs Instance Attributes

Instance attribute:

Defined inside __init__.
Unique for each object.

self.name

Class attribute:

Shared by all objects.

Show/Hide Code
class Dog:
    species = "Canine"   # class attribute

Every object sees the same value unless overridden.

Example with Both Types

Show/Hide Code
class Dog:
    species = "Canine"         # class attribute

    def __init__(self, name):
        self.name = name       # instance attribute

fido = Dog("Fido")
buddy = Dog("Buddy")

print(fido.species)   # Canine
print(buddy.species)  # Canine
Canine
Canine

20.4 Inheritance

A class can inherit from another class:

Show/Hide Code
class Animal:
    def walk(self):
        print("Walking...")

Child class:

Show/Hide Code
class Dog(Animal):
    def bark(self):
        print("WOF!")

Using the inherited method:

Show/Hide Code
Max= Dog()
Max.walk()
Max.bark()
Walking...
WOF!

Method Overriding

A child class can replace a parent class method:

Show/Hide Code
class Animal:
    def speak(self):
        print("Animal sound")

class Dog(Animal):
    def speak(self):
        print("WOF!")

Using super()

You can call the parent class method inside the child class:

Show/Hide Code
class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

20.5 Summary

✓ Classes define custom data types
✓ Objects are instances of classes
self references the current instance
__init__ initializes new objects
✓ Attributes hold data
✓ Methods perform actions
✓ Inheritance allows code reuse

Try it

Q 1

Create a class Person with:

  • attributes name and age

  • method say_hello() that prints:
    "Hello, my name is <name>"

Q 2

Create a class Car with:

  • attributes: brand, year

  • method info() that prints:
    "Brand: <brand>, Year: <year>"

Q 3

Create a base class Animal with method sound(). Create a class Cat that inherits from it and overrides sound() with "Meow".

21 Modules in Python

As your programs grow larger, organizing everything in a single file becomes messy. Modules help us structure code by splitting it into multiple files, each containing related functions, classes, and variables.

What Is a Module?

A module is simply a Python file (.py) that contains code.

Examples:

  • math.py → a module

  • dog.py → a module

  • utils/helpers.py → a module inside a folder

Python itself comes with many built-in modules like:

  • math (Provides mathematical functions and constants.)

  • random (Generates random numbers and performs random operations.)

  • os (Enables interaction with the operating system (files, paths, processes).

  • datetime (Works with dates, times, and time intervals.)

You have used modules already, whenever you write:

Show/Hide Code
import math

You are importing a module.

Why Use Modules?

Modules help you:

  • Organize code into logical pieces

  • Reuse code across multiple files

  • Collaborate with others on large projects

  • Avoid extremely long Python files

21.1 Creating Your Own Module

Let’s create a simple module called dog.py.

dog.py

Show/Hide Code
def bark():
    print("WOF!")

This file defines a single function, bark().

21.2 Importing a Module

To use the code inside dog.py, you import it in another file.

main.py

Show/Hide Code
import dog
dog.bark()

21.2.1 How it works:

  • import dog loads the module

  • dog.bark() calls the function using dot notation:

module_name.function_name

21.3 Importing Specific Items

Instead of importing the whole module, you can import just what you need.

main2.py

Show/Hide Code
from dog import bark

bark()   # No need for dog.bark()
WOF!

When to use which?

Style When to Use
import dog When you want many functions or want to keep clear namespaces
from dog import bark When you only need one item and want clean code

21.4 Module Organization in Folders

Projects often look like this:

Why is __init__.py required?

It signals to Python that this folder is a package, meaning it contains modules.

Importing From a Folder

Option A: Import the module

Show/Hide Code
from lib import dog

dog.bark()
WOF!

Option B: Import a specific function

Show/Hide Code
from lib.dog import bark

bark()
WOF!

21.5 Importing Multiple Things

dog2.py

Show/Hide Code
def bark():
    print("WOF!")

def sleep():
    print("zzzz...")

main2.py

Show/Hide Code
from dog2 import bark, sleep

bark()
sleep()
WOF!
zzzz...

21.6 Renaming Imported Modules / Functions

Module alias

Show/Hide Code
import dog2 as d

d.bark()
WOF!

Function alias

Show/Hide Code
from dog2 import bark as b

b()
WOF!

Useful when modules have long names.

21.7 Built-In Modules Examples

Math operations

Show/Hide Code
import math
print(math.sqrt(49))
7.0

Random numbers

Show/Hide Code
import random
print(random.randint(1, 100))
58

Dates and times

Show/Hide Code
from datetime import datetime
print(datetime.now())
2026-01-20 17:18:18.411514

21.8 Executing Modules as Scripts

Sometimes a module contains:

Show/Hide Code
def bark():
    print("WOF!")

if __name__ == "__main__":
    bark()
WOF!

This allows:

  • Running the file directly → it barks

  • Importing it in another file → it does NOT bark automatically

This is very common in real projects.

21.9 Common Errors When Working With Modules

Error Meaning
ModuleNotFoundError Python cannot find the file/folder
ImportError The function or class does not exist in the module
Missing __init__.py Python treats the folder as normal, not a package
Circular import Two modules import each other

22 The Python Standard Library (Expanded Version)

Python comes with a huge collection of ready-to-use modules called the Python Standard Library.
You don’t need to install anything, these modules come built-in with every Python installation.

Think of the Standard Library as Python’s toolbox:

✔ tools for math ✔ tools for dates ✔ tools for file operations ✔ tools for databases ✔ tools for networking ✔ tools for random numbers ✔ tools for statistics ✔ tools for text processing ✔ tools for debugging …and hundreds of others.

22.1 What Is the Standard Library?

A massive set of modules written in Python and C, included by default.

You can see the full official list here: https://docs.python.org/3/library/

It includes over 200 modules.

Why the Standard Library Is Important

Because it allows you to:

  • Avoid reinventing the wheel

  • Build powerful programs quickly

  • Access system-level features (files, OS, networking)

  • Manipulate data formats (JSON, CSV, XML)

  • Work with time, dates, numbers, randomness

  • Do advanced computation

Many beginners don’t know how powerful it is, professional developers use the standard library daily.

22.2 Common and Useful Standard Library Modules

Below is a short overview of the most important ones.

  1. math – Mathematical Functions

Used for:

  • Square roots

  • Trigonometry

  • Powers

  • Constants like pi (π)

Example:

Show/Hide Code
import math

print(math.sqrt(9))
print(math.pi)
print(math.sin(0))
3.0
3.141592653589793
0.0
  1. random – Random Number Generation

Used for:

  • Random integers

  • Random floats

  • Random selection

  • Shuffling lists

Example:

Show/Hide Code
import random

print(random.randint(1, 10))
print(random.choice(["dog", "cat", "mouse"]))
6
mouse
  1. datetime – Dates and Times

Used for:

  • Getting current date/time

  • Date math

  • Formatting timestamps

Example:

Show/Hide Code
from datetime import datetime

now = datetime.now()
print(now)
2026-01-20 17:18:18.450678
  1. json – Working with JSON Data

Essential for APIs and web data.

Show/Hide Code
import json

data = '{"name": "Alice", "age": 25}'
obj = json.loads(data)  # JSON → Python dict
print(obj["name"])
Alice
  1. os – Operating System Utilities

Used for:

  • Creating/deleting folders

  • Listing files

  • Environment variables

Example:

Show/Hide Code
import os

print(os.listdir())   # list files in the current directory
  1. re – Regular Expressions

Used for searching text patterns.

Show/Hide Code
import re

match = re.search(r"\d+", "My number is 173")
print(match.group())   # 173
173
  1. statistics – Statistical Functions

Used for:

  • Mean

  • Median

  • Variance

  • Standard deviation

Example:

Show/Hide Code
import statistics

print(statistics.mean([1, 2, 3, 4]))
2.5
  1. sqlite3 – Built-in Database

Python includes an SQL database with no installation.

Show/Hide Code
import sqlite3

conn = sqlite3.connect("data.db")

This is extremely useful for small applications.

  1. urllib – URL Handling

Used for downloading data or handling URLs.

Note: requests is NOT part of the standard library

Your original notes say:

requests to perform HTTP network requests

However, requests is NOT in the Python standard library.
It is the most popular external library, but you must install it:

Show/Hide Code
pip install requests

To stay accurate, replace it with:

  • urllib.request → standard library alternative

22.3 Importing Standard Library Modules

You import them exactly like your own modules:

Option 1: Import the whole module

Show/Hide Code
import math

print(math.sqrt(4))
2.0

Option 2: Import a specific function

Show/Hide Code
from math import sqrt

print(sqrt(4))
2.0

Same rules as custom modules.

23 The PEP 8 Python Style Guide

Writing clean and readable code is a fundamental skill for every Python developer. Even though Python is known for being simple and expressive, your code can still become messy if you ignore consistent formatting.

To help developers write clean and readable code, Python has an official style guide called PEP 8.

What is PEP 8?

PEP 8 stands for Python Enhancement Proposal 8.
It defines the coding style conventions that all Python programmers are encouraged to follow.

These conventions cover:

  • indentation

  • naming rules

  • code layout

  • whitespace

  • comments

  • imports

  • line length

  • overall readability

PEP 8 helps maintain a consistent look across Python projects, making your code easier for others to understand, and easier for future you to understand too.

You can read the full guide here: https://www.python.org/dev/peps/pep-0008/

23.1 Why Follow PEP 8?

Following PEP 8 helps you:

✔ Write code that looks professional
✔ Make your programs easier to maintain
✔ Work smoothly with teams
✔ Reduce confusion and errors
✔ Learn the “Pythonic” way of coding

Good code isn’t just correct, it’s readable.

23.2 Key PEP 8 Guidelines

Below are the most important rules you should start applying from day one.

  1. Indentation
  • Use 4 spaces per indentation level

  • Never use tabs

  • Never mix tabs and spaces

Example:

Show/Hide Code
def greet():
    print("Hello")
  1. Encoding

Python files should use:

UTF-8

This ensures all characters and symbols work correctly.

  1. Line Length
  • Maximum recommended line length: 80 characters

  • Modern codebases sometimes use up to 120, but 80 is the standard in training and exams

This helps avoid horizontal scrolling and improves readability.

  1. One Statement per Line

Bad:

Show/Hide Code
x = 5; y = 10

Good

Show/Hide Code
x = 5
y = 10
  1. Naming Conventions

snake_case for:

  • functions

  • variables

  • file names

Show/Hide Code
def calculate_total():
    user_age = 21

CamelCase for classes:

Show/Hide Code
class DataProcessor:
    pass

UPPERCASE for constants:

Show/Hide Code
MAX_USERS = 200
PI = 3.14

lowercase (no underscores) for package names:

mypackage
tools
datascience
  1. Use Meaningful Names

Bad:

Show/Hide Code
a = 10
b = 3

Good:

Show/Hide Code
height = 10
retry_delay = 3
  1. Whitespace Rules

Add spaces around operators:

Correct:

Show/Hide Code
x = a + b * 2

Wrong:

Show/Hide Code
x=a+b*2

Avoid unnecessary spaces:

Wrong:

Show/Hide Code
print(  "Hello"  )
Hello

Correct:

Show/Hide Code
print("Hello")
Hello
  1. Comments

Good comments explain why, not what.

Bad:

Show/Hide Code
x = x + 1  # add 1 to x

Good:

Show/Hide Code
# Increase retry count after failed attempt
retry_count = retry_count + 1

Avoid obvious comments and focus on clarifying logic or intent.

  1. Blank Lines

Blank lines help structure your code.

  • Add one blank line before each function

  • Add one blank line between methods inside a class

  • Inside a function, use blank lines to separate logical parts

Example:

Show/Hide Code
def load_data():
    data = read_file()

    cleaned = clean(data)

    return cleaned
  1. Spaces and Structure (Very Important)
  • No extra spaces before commas:

❌ my_list = [1 , 2 , 3]

✔ my_list = [1, 2, 3]

  • No space before function parentheses:

❌ print (“Hello”)

✔ print(“Hello”)

  1. Imports

PEP 8 recommends this order:

  1. Standard library imports

  2. Third-party imports

  3. Local imports

Example:

Show/Hide Code
import os
import sys

import numpy as np
import requests

from dog2 import bark as b

23.3 Summary

PEP 8 teaches you how to write clean, readable, and standardized Python code.
Following it from the beginning will make you a better programmer and prepare you for real-world projects, teamwork, and professional development.

24 Debugging in Python

Debugging is one of the most important skills every programmer must learn. No matter how careful you are, your code will eventually have bugs, mistakes, unexpected behavior, or logical errors.

Knowing how to debug will save you hours of frustration and make you a stronger Python developer.

Python provides a built-in debugger called pdb (Python Debugger), which lets you pause your program, inspect variables, and run code step by step.

What Is Debugging?

Debugging means:

  • finding errors

  • understanding why they happen

  • fixing them

  • preventing them in the future

Debugging is not guesswork, it’s a structured investigation of how your code behaves.

24.1 Using Python’s Built-in Debugger (pdb)

Python makes debugging easy with the built-in command:

Show/Hide Code
breakpoint()

When your code reaches a breakpoint(), execution pauses and the debugger opens.

You can place as many breakpoints as you want in different parts of your code.

Example:

Show/Hide Code
x = 10
breakpoint()
y = 20
print(x + y)
30

When Python reaches breakpoint(), it stops and waits for your commands.

24.2 What Happens When Execution Stops ?

When the debugger activates, you enter an interactive mode where you can:

  • inspect variable values

  • move through the code

  • run commands

  • continue execution

This is extremely useful when you’re trying to understand why something behaves incorrectly

24.3 Useful Debugger Commands

Here are the commands you will use most often in pdb:

Inspect a Variable

Just type its name:

(Pdb) x

This prints the current value of x.

n — Next line

n runs the next line of code within the current function

(Pdb) n

This is good when you want to advance step-by-step but avoid entering functions.

s — Step into

s steps into a function call if the next line is a function.

(Pdb) s

Use this when you want to debug inside another function.

c — Continue

c runs the program normally until:

  • the next breakpoint

  • or the program finishes

(Pdb) c

Use this when you’re done inspecting the current part.

q — Quit

q stops the debugger and terminates your program immediately.

(Pdb) q

Why Debugging Is Important

Debugging helps you:

✔ Understand why errors happen
✔ See exactly what your code is doing
✔ Check the value of variables at any moment
✔ Solve tricky logic issues
✔ Avoid print()-based debugging (which gets messy)

For complex loops, recursion, or unexpected behavior, debugging is often the fastest way to find the problem.

Example: Debugging a Loop Problem
Show/Hide Code
numbers = [1, 2, 3, 4]
total = 0

for n in numbers:
    breakpoint()
    total = total + n

print(total)
10

At each iteration, the debugger stops. You can:

  • inspect n

  • inspect total

  • step through the loop

  • watch how values change

This helps you understand the flow of the loop.

Debugging Tips
  • Add breakpoints just before the part where things go wrong

  • Check variable values often

  • Use n to follow execution in order

  • Use s to go inside suspicious functions

  • Avoid guessing, debug logically

25 Variable Scope in Python

When you create a variable in Python, that variable is only accessible (or visible) within a certain part of your program. This concept is called variable scope.

Understanding scope is important because it helps you avoid errors and makes your code easier to understand and maintain.

Python has four levels of scope, following the LEGB rule:

  • L → Local

  • E → Enclosing

  • G → Global

  • B → Built-in

We’ll explain each one step by step.

25.1 Global Scope

A variable defined outside of all functions belongs to the global scope.
It can be accessed by any function as long as you don’t redefine it inside the function .

Show/Hide Code
age = 8   # global variabl

def test():
  print(age)

print(age)
test()

Here, age is global and both the main program and the function can see it.

25.2 Local Scope

If you define a variable inside a function, it belongs to the function’s local scope.

This means:

  • It exists only while the function is running

  • It cannot be accessed outside the function

Example:

Show/Hide Code
def test():
    age = 8   # local variable
    print(age)

test()       # 8
print(age)   # Error! age is not defined

Trying to access age outside the function gives a NameError.

Enclosing Scope (Inner/Nested Functions)

If you have a function inside another function, the inner function can access variables from the outer function.

Example:

Show/Hide Code
def outer():
    x = 10

    def inner():
        print(x)   # can access outer variable

    inner()

outer()
  • x is local to outer()

  • but it is also enclosing for inner()

This is the “E” in the LEGB rule.

Built-in Scope

Python has many built-in functions and keywords like:

Show/Hide Code
len, print, range, list, dict
(<function len(obj, /)>,
 <function print(*args, sep=' ', end='\n', file=None, flush=False)>,
 range,
 list,
 dict)

These are always available unless you override them (which you should avoid).

Example:

Show/Hide Code
print(len("hello"))
5

Python finds these names in the built-in scope.

25.3 Important: Using Global Variables Inside Functions

If you want to change a global variable inside a function, you must declare it using the global keyword:

Show/Hide Code
counter = 0

def increase():
    global counter
    counter += 1

increase()
print(counter)
1

Without global, Python thinks you want to create a new local variable, leading to an error.
Best Practice: Avoid Too Many Globals

While global variables work, relying on them too much makes your code:

  • harder to debug

  • harder to understand

  • more likely to break as programs grow

Prefer function parameters instead of globals when possible.

Summary

Scope Level Where Defined Accessible In
Local Inside function Only inside that function
Enclosing Outer function of nested functions Inner functions
Global Top level of file Entire file + functions (read-only unless global)
Built-in Python interpreter Everywhere

26 How to Accept Arguments from the Command Line in Python

When you run a Python program from the terminal, you can pass extra values called command-line arguments directly at execution time.

This allows you to:

  • configure how the program behaves

  • pass input without modifying code

  • build real scripts and utilities like CLI tools

Example:

python app.py hello
python app.py 10 20
python app.py -c red

Python provides multiple ways to read and process these arguments.

26.1 Using sys.argv (Basic Method)

The simplest way to access command-line arguments is using the sys module.

Show/Hide Code
import sys

print(len(sys.argv))
print(sys.argv)
4
['C:\\Users\\NIST\\AppData\\Roaming\\Python\\Python312\\site-packages\\ipykernel_launcher.py', '-f', 'C:\\Users\\NIST\\AppData\\Local\\Temp\\tmpk3i9reid.json', '--HistoryManager.hist_file=:memory:']
  • sys.argv is a list of strings

  • The first item (sys.argv[0]) is always the script file name

  • Following items are the arguments typed by the user

Example:

python main.py blue 20

sys.argv becomes:

['main.py', 'blue', '20']

Limitations of sys.argv

  • You must manually check if arguments exist

  • No automatic error messages

  • No automatic help (-h)

  • All arguments are strings, you must convert manually

Because of these limitations, professional Python CLI tools use argparse.

26.3 Restricting Allowed Values

You can restrict an option using choices=:

Show/Hide Code
parser.add_argument(
    '-c', '--color',
    required=True,
    choices={'red', 'yellow'},
    help='the color to search for'
)

Running with invalid input:

python program.py -c blue

Produces:

argument -c/--color: invalid choice: 'blue' (choose from 'yellow', 'red')

26.4 Other argument types (Brief Intro)

argparse supports:

  • positional arguments
parser.add_argument('filename')
  • automatic type conversion
parser.add_argument('--age', type=int)
  • default values
parser.add_argument('--level', default='info')
  • boolean flags
parser.add_argument('--debug', action='store_true')

26.5 Alternative Libraries

Some popular third-party CLI frameworks:

Library Notes
Click Very popular, clean syntax, good for production CLIs
Typer Built on Click; modern & easy; used for AI apps
Prompt Toolkit For interactive shells (menus, inputs, autocompletion)

But argparse is built-in and perfectly fine for most scripts.

27 Lambda Functions in Python

In Python, a lambda function (also called an anonymous function) is a small, unnamed function defined using the lambda keyword.

It is useful when you need a quick function for a short period of time, especially when passing functions to other functions like map(), filter(), or sorted().

27.1 Basic Syntax

A lambda function looks like this:

lambda <arguments> : <expression>
  • It can take any number of arguments

  • It must contain exactly one expression

  • It automatically returns the result of that expression

  • It cannot contain statements (like for, return, or print inside it)

Example:

Show/Hide Code
lambda num: num * 2
<function __main__.<lambda>(num)>

This function doubles the input value.

27.2 Assigning Lambda Functions to a Variable

Although lambda functions have no name, you can store them in a variable:

Show/Hide Code
multiply = lambda a, b: a * b

print(multiply(2, 2))
4

This works exactly like a normal function:

Show/Hide Code
def multiply(a, b):
    return a * b

But lambda is shorter and lightweight.

27.3 Multiple Arguments

Lambda functions can take multiple arguments:

Show/Hide Code
sum_three = lambda x, y, z: x + y + z
print(sum_three(1, 2, 3))   # 6
6

27.4 Why Lambdas Are Useful

The true power of lambda functions appears when used with built-in Python functions that expect another function as an argument.

a. Using lambda with map()

map() applies a function to every element in a list.

Show/Hide Code
numbers = [1, 2, 3, 4]
doubled = list(map(lambda n: n * 2, numbers))

print(doubled)
[2, 4, 6, 8]

b. Using lambda with filter()

filter() keeps only the elements that match a condition.

Show/Hide Code
numbers = [1, 2, 3, 4, 5, 6]
evens = list(filter(lambda n: n % 2 == 0, numbers))

print(evens)
[2, 4, 6]

c. Using lambda with sorted()

Sorting items by a custom key:

Show/Hide Code
students = [('Alice', 25), ('Bob', 20), ('Chris', 22)]
sorted_by_age = sorted(students, key=lambda s: s[1])

print(sorted_by_age)
[('Bob', 20), ('Chris', 22), ('Alice', 25)]

d. Using lambda with reduce() (from functools)

reduce() combines elements into a single value.

Show/Hide Code
from functools import reduce

result = reduce(lambda a, b: a + b, [1, 2, 3, 4])
print(result)
10
When NOT to use lambda

Use a normal function instead of a lambda when:

  • the logic needs multiple lines

  • the function is reused many times

  • readability is important

  • you need type hints or documentation

Lambda functions are for quick, short operations.

28 Recursion in Python

Recursion is a programming technique where a function calls itself to solve a problem.
It is especially useful when a problem can be broken down into smaller subproblems of the same kind.

Recursion is often used in:

  • mathematical computations (factorials, Fibonacci numbers)

  • navigating nested structures (folders, trees)

  • divide-and-conquer algorithms (quick sort, merge sort)

  • processing hierarchical data (JSON, DOM trees)

28.1 What Makes a Recursive Function?

Every recursive function needs two essential components:

✔ Base Case

A condition that stops the recursion.

✔ Recursive Case

The part where the function calls itself with a smaller problem.

Without a base case, the function would keep calling itself forever.

28.2 Example: Factorial Using Recursion

The factorial of a number multiplies the number by all positive integers below it:

  • 3! = 3 × 2 × 1 = 6

  • 5! = 5 × 4 × 3 × 2 × 1 = 120

Recursive definition of factorial:

  • Base case: 1! = 1

  • Recursive case: n! = n × (n − 1)!

Here’s the recursive implementation:

Show/Hide Code
def factorial(n):
    if n == 1:
        return 1  # base case
    return n * factorial(n - 1)   # recursive case

print(factorial(3))
print(factorial(4))
print(factorial(5))  
6
24
120

28.3 Important Warning: Infinite Recursion

If you mistakenly write:

factorial(n)

instead of

factorial(n - 1)

you create an infinite loop of recursive calls.

Python will stop after around 1000 recursive calls and raise:

RecursionError: maximum recursion depth exceeded

This is Python’s safety limit to prevent your program from crashing.

28.4 Another Example: Recursive Sum

Here is a simple recursive function to sum numbers from 1 to n:

Show/Hide Code
def recursive_sum(n):
    if n == 1:
        return 1
    return n + recursive_sum(n - 1)

print(recursive_sum(5))
15

28.5 Recursion vs Iteration

Recursion Iteration (loops)
Elegant and shorter Often faster
Easier for tree-like problems Uses less memory
Risk of recursion limit No recursion limit

Use recursion when the problem naturally fits a recursive structure.

28.6 When Recursion is Useful

Recursion shines in situations like:

✔ Problem can be divided into smaller identical problems

Example: factorial, Fibonacci, power, sum of digits

✔ Data is hierarchical

Example: folders inside folders, tree data structures

✔ Algorithms that depend on divide-and-conquer

Example: quicksort, mergesort, binary search

29 Nested Functions in Python

In Python, you can define a function inside another function.
These are called nested functions or inner functions.

Nested functions are important because they allow you to:

  • encapsulate helper logic that should not be accessed outside

  • keep your code organized

  • use closures (functions that remember values)

29.1 Why Use Nested Functions?

✔ Encapsulation

You can create small helper functions that only make sense inside a specific outer function.

✔ Avoid Naming Conflicts

Since the inner function is not visible outside, it won’t clash with function names elsewhere.

✔ Closures

Inner functions can remember and use values from the outer function’s scope (explained fully in the next chapter).

29.2 Basic Example of a Nested Function

Show/Hide Code
def talk(phrase):
    def say(word):
        print(word)

    words = phrase.split(' ')
    for word in words:
        say(word)

talk('I am going to buy the milk')
I
am
going
to
buy
the
milk

29.2.1 How it works:

  • talk() is the outer function

  • say() is the inner function

  • say() is only available inside talk()

  • talk() splits the phrase and uses say() to print each word individually

Try calling say() outside the function — it will cause an error.

29.3 Accessing Variables from the Outer Function

Inner functions can read variables from the outer function with no problem.

But to modify a variable from the outer function, you must declare it as nonlocal.

Example:

Show/Hide Code
def count():
    count = 0

    def increment():
        nonlocal count
        count = count + 1
        print(count)

    increment()

count()
1

Why nonlocal?

  • Without nonlocal, Python thinks count inside increment() is a new local variable, not the one in the outer function.

  • With nonlocal, we tell Python:
    “Use the variable from the outer function.”

29.4 When Are Nested Functions Useful?

✔ When a helper function is only relevant inside another function

Example: text parsing, repeated calculations, input validation

✔ When using closures

Inner functions can capture values from the outer function.
This becomes powerful when returning functions.

✔ Creating function generators

Example: creating a function that returns another function with some pre-configured behavior.

Simple Use Case Example

Show/Hide Code
def greeting(name):
    def message():
        print("Hello,", name)
    message()

greeting("Prosper")
Hello, Prosper

he inner function message() remembers name from its outer function.

30 Closures in Python

A closure is one of the most powerful features in Python.
It happens when:

  1. A function is defined inside another function (nested function)

  2. The inner function captures variables from the outer function

  3. The outer function returns the inner function

  4. The returned inner function continues to remember the variables even after the outer function has finished running

In simple terms:

A closure is a function that remembers values from the environment it was created in.

30.1 Why Closures Are Useful

Closures allow you to:

✔ Preserve state without using classes

You can create functions that keep their own memory.

✔ Build function factories

You can make functions that generate other customized functions.

✔ Encapsulate logic and data

Only the inner function has access to certain variables (automatic data hiding).

30.2 The Classic Example: A Closure Counter

Here’s the counter code from the original text, now fully explained:

Show/Hide Code
def counter():
    count = 0   # local variable

    def increment():
        nonlocal count
        count = count + 1
        return count

    return increment

Explanation:

  • count is defined in counter()

  • increment() is defined inside counter()

  • increment() uses the variable count

  • counter() returns the inner function but count does not disappear

  • Each call to increment() updates and returns the preserved value

Using the closure:

Show/Hide Code
increment = counter()

print(increment())
print(increment())
print(increment())
1
2
3

Even though counter() finished long ago, increment() still remembers count.

Visualizing What Happens

When calling increment = counter()

  • counter() runs

  • count = 0 is created

  • increment() is created

  • increment() forms a closure capturing count

  • counter() returns increment()

  • increment is now a function that carries its own “state”

30.3 Creating Multiple Independent Closures

Each call to counter() creates a new independent closure:

Show/Hide Code
c1 = counter()
c2 = counter()

print(c1())
print(c1())

print(c2())
print(c2())
1
2
1
2

30.4 Another Example: Make Your Own Multiplier

Closures can be used to build function factories:

Show/Hide Code
def multiply_by(n):
    def multiply(x):
        return x * n
    return multiply

double = multiply_by(2)
triple = multiply_by(3)

print(double(5))
print(triple(5))
10
15

Here:

  • n is captured by the inner function

  • double remembers n = 2

  • triple remembers n = 3

30.5 When to Use Closures

Closures are great when you want to:

✔ Keep state without using a class

Like counters, accumulators, or configuration holders.

✔ Create customized functions

E.g., discount calculators, mathematical functions, validators.

✔ Encapsulate and protect variables

Variables inside closures cannot be accessed directly from outside.

31 Decorators in Python

A decorator in Python is a special tool that lets you modify or enhance how a function behaves without changing the function’s original code.

Decorators are commonly used to:

  • Add logging

  • Measure execution time

  • Check permissions

  • Add caching

  • Validate inputs

    …and much more.

31.1 How Decorators Work

A decorator is applied using the @ symbol right above the function you want to modify:

Show/Hide Code
@logtime
def hello():
    print("hello!")

This means:

“Wrap the function hello() with the decorator logtime.”

So every time you call hello(), Python actually calls logtime(hello).

31.1.1 Creating a Decorator

A decorator is a function that:

  1. Takes another function as input

  2. Defines an inner function (the wrapper) that adds extra behavior

  3. Calls the original function inside the wrapper

  4. Returns the wrapper

Example:

Show/Hide Code
def logtime(func):
    def wrapper():
        print("Before calling function...")
        value = func()
        print("After calling function...")
        return value
    return wrapper

Now you can apply it:

Show/Hide Code
@logtime
def greet():
    print("Hello!")

Calling:

Show/Hide Code
greet()
Before calling function...
Hello!
After calling function...

The original function runs, but with extra steps added before and after.

31.2 Why Use Decorators?

Because they help you separate reusable behavior from your main logic.

For example, if you want multiple functions to log their execution time, you can write a single decorator instead of copying code into every function.

31.3 Decorators with Arguments

If your original function accepts parameters, your wrapper must be flexible too:

Show/Hide Code
def debug(func):
    def wrapper(*args, **kwargs):
        print("Function called with:", args, kwargs)
        return func(*args, **kwargs)
    return wrapper

Usage:

Show/Hide Code
@debug
def add(a, b):
    return a + b

print(add(3, 4))
Function called with: (3, 4) {}
7

31.4 Closures + Decorators

Decorators rely on closures (from previous chapter), because the inner wrapper function remembers the original function reference even after the decorator finishes running.

32 Docstrings in Python

Docstrings (documentation strings) are special strings in Python used to describe what a function, class, method, or module does.

Good documentation is important because:

  • It helps other people understand your code.

  • It helps YOU in the future understand what you were thinking.

  • It allows tools to automatically extract the documentation.

  • It keeps your code clean and professional.

Comments vs Docstrings

Comments use #:

Show/Hide Code
# This is a comment
num = 1  # This is another comment

Docstrings are triple-quoted strings placed inside functions, classes, or modules.

Docstrings follow conventions, so documentation tools can extract and use them.

Function Docstrings

A docstring is the first string inside a function, written with triple quotes:

Show/Hide Code
def increment(n):
    """Increment a number by 1."""
    return n + 1

This docstring tells anyone using the function what it does.

Class and Method Docstrings

You can document classes and their methods the same way:

Show/Hide Code
class Dog:
    """A class representing a dog."""

    def __init__(self, name, age):
        """Initialize a new Dog with name and age."""
        self.name = name
        self.age = age

    def bark(self):
        """Let the dog bark."""
        print("WOF!")

Module Docstrings

A module (a Python file) can also start with a docstring:

Show/Hide Code
"""
Dog module

This module defines the Dog class and related utilities.
"""
'\nDog module\n\nThis module defines the Dog class and related utilities.\n'

Then your class definition follows.

Multiline Docstrings

Docstrings can span multiple lines:

Show/Hide Code
def increment(n):
    """
    Increment
    a number
    by 1.
    """
    return n + 1

Using help() to Read Docstrings

Python uses docstrings when you call the built-in help() function:

Show/Hide Code
help(increment)
Help on function increment in module __main__:

increment(n)
    Increment
    a number
    by 1.

This is extremely useful when exploring unfamiliar code.

Docstring Formats / Styles

There are several standard formats:

  • Google Style (very popular)

  • NumPy / SciPy Style

  • reStructuredText (Sphinx)

Google Style example:

Show/Hide Code
def add(a, b):
    """
    Add two numbers.

    Args:
        a (int): First number.
        b (int): Second number.

    Returns:
        int: The sum of a and b.
    """
    return a + b

These standards allow tools like Sphinx or pdoc to automatically generate full documentation websites from your code.

33 Introspection in Python

Introspection means examining objects at runtime, in other words, asking Python questions about functions, objects, variables, and modules while the program is running.

Python is highly introspective, which makes it very powerful for debugging, learning, and metaprogramming.

33.1 Using help()

The built-in help() function displays the documentation (from docstrings):

Show/Hide Code
def increment(n):
    return n + 1

print(increment)
# <function increment at 0x7f420e2973a0>
<function increment at 0x000001E1BA9E32E0>

This is useful for quick reference.

33.2 Using print() to inspect objects

If you print a function, Python shows information about its memory location:

Show/Hide Code
def increment(n):
    return n + 1

print(increment)
# <function increment at 0x7f420e2973a0>
<function increment at 0x000001E1BA9E27A0>

Same for objects:

Show/Hide Code
class Dog:
    def bark(self):
        print("WOF!")

Max = Dog()
print(Max)
# <__main__.Dog object at 0x7f42099d3340>
<__main__.Dog object at 0x000001E1BA933A70>

33.3 Using type()

The type() function shows the type/class of any object:

Show/Hide Code
print(type(increment))
print(type(Max))
print(type(1))
print(type('test'))
<class 'function'>
<class '__main__.Dog'>
<class 'int'>
<class 'str'>

33.4 Using dir()

dir() lists all attributes and methods an object has:

Show/Hide Code
print(dir(Max))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'bark']

33.5 Using id()

id() shows the memory address (identity) of an object:

Show/Hide Code
print(id(Max))
print(id(1))
2069009480304
140723354798520

You can use this to check if two variables reference the same object.

33.6 The inspect Module (Advanced Introspection)

The inspect module gives much more detailed information, such as:

  • getting the source code of a function

  • listing parameters

  • seeing which module something came from

  • checking if a function is a coroutine, generator, etc.

Documentation: https://docs.python.org/3/library/inspect.html

34 Annotations in Python

Python is a dynamically typed language, meaning that variables and functions do not require a declared type. You can write:

Show/Hide Code
def increment(n):
    return n + 1

and Python will infer the type at runtime.

However, as programs grow larger, it becomes useful to document expected types. This is where annotations come in.

  1. Function Annotations

Annotations let you optionally specify:

  • the type of each parameter

  • the type of the return value

Example:

Show/Hide Code
def increment(n: int) -> int:
    return n + 1

Here:

  • n: int means we expect n to be an integer

  • -> int means the function should return an integer

Python does not enforce these rules—they are only hints.

  1. Variable Annotations

Variables can also be annotated:

Show/Hide Code
count: int = 0
name: str = "Alice"
my_list: list[int] = [1, 2, 3]

These annotations are purely informational.

  1. Why Use Type Annotations?

Even though Python ignores them at runtime, annotations help:

✔ Static type checking

Tools like mypy, pyright, or IDEs (VS Code, PyCharm) can check your code before you run it, helping catch errors early.

Example:

mypy my_program.py

✔ Better auto-completion

Editors can provide smarter suggestions when types are known.

✔ Clearer code

Types make functions easier to understand, especially in big projects.

  1. Example with Multiple Parameters
Show/Hide Code
def greet(name: str, age: int) -> str:
    return f"Hello {name}, you are {age} years old."
  1. Using Optional and Union Types

Python also supports more complex annotations:

Show/Hide Code
from typing import Optional, Union

def parse_age(age: Optional[int]) -> Union[int, None]:
    return age
  1. Summary
  • Type annotations are optional, not enforced by Python.

  • They improve readability and help external tools catch bugs early.

  • They are especially useful for large codebases and refactoring.

35 Exceptions in Python

Programs often encounter unexpected situations: missing files, invalid user input, network errors, division by zero, and more.
To prevent your program from crashing, Python provides a structured way to detect and handle errors using exceptions.

The try Block

You wrap code that might cause an error inside a try: block:

Show/Hide Code
try:
    # code that may raise an error

If no error occurs, the program continues normally.

The except Block

If the code inside try: raises an exception, Python jumps into the appropriate except: block:

Show/Hide Code
try:
    # some lines of code
except ERROR1:
    # handle ERROR1
except ERROR2:
    # handle ERROR2

Each except block handles a specific type of exception.

Catching All Exceptions

You can catch any exception using a bare except::

Show/Hide Code
try:
    # risky code
except ERROR1:
    # handle ERROR1
except:
    # handle all other exceptions

This should be used sparingly, because it hides all error types.

The else Block

The else: block runs only if no exception happened in the try: block:

Show/Hide Code
try:  
    # code
except ERROR1:
    # handle ERROR1
except ERROR2:
    # handle ERROR2
else:
    # runs only when no exceptions occur

This is great for code that should execute only if everything went well.

The finally Block

The finally: block always runs, whether an exception occurred or not:

Show/Hide Code
try:
    # code
except ERROR1:
    # handler
else:
    # executed if no errors
finally:
    # always executed

Typical use cases include:

  • closing files

  • closing database connections

  • cleaning resources

35.1 Common Python Exceptions

Different operations raise different exceptions:

Error When It Happens
ZeroDivisionError dividing by zero
TypeError wrong data type used
ValueError invalid value passed
EOFError reading file input past the end
IndexError using an invalid list index

Example:

Show/Hide Code
reult = 2 / 0 # this gives an error

The program stops and does not execute further lines unless handled.

Handling an Exception

Show/Hide Code
try:
    result = 2 / 0
except ZeroDivisionError:
    print('Cannot divide by zero!')
finally:
    result = 1

print(result)
Cannot divide by zero!
1

The program recovers gracefully.

Raising Your Own Exceptions

Use the raise keyword:

Show/Hide Code
raise Exception('An error occurred!')

Handling it:

Show/Hide Code
try:
    raise Exception('An error occurred!')
except Exception as error:
    print(error)
An error occurred!

35.2 Creating Custom Exceptions

You can define your own exception classes by extending Python’s built-in Exception class:

Show/Hide Code
class DogNotFoundException(Exception):
    pass

Using it:

Show/Hide Code
try:
    raise DogNotFoundException()
except DogNotFoundException:
    print('Dog not found!')
Dog not found!

Custom exceptions help you create meaningful, domain-specific error handling logic.

36 The with Statement in Python

Many operations in programming require setting something up, doing some work, and then cleaning it up afterward.
For example:

  • opening and closing a file

  • acquiring and releasing a lock

  • connecting and disconnecting from a database

If you forget the cleanup step, your program may leak resources or behave incorrectly.

Python provides the with statement to manage these tasks cleanly and safely.
It ensures that necessary cleanup happens automatically, even if errors occur.

36.1 Without with: Manual Resource Handling

When opening files, for example, you must explicitly remember to close the file:

Show/Hide Code
filename = 'test.txt'

try:
    file = open(filename, 'r')
    content = file.read()
    print(content)
finally:
    file.close()
I am Prosper, and this is testing
the text you see is from tezt.txt

Using try...finally ensures the file closes even if an exception occurs.

But this is verbose and easy to forget.

36.2 Using with: Automatic Cleanup

The with statement simplifies the pattern:

Show/Hide Code
filename = 'test.txt'

with open(filename, 'r') as file:
    content = file.read()
    print(content)
I am Prosper, and this is testing
the text you see is from tezt.txt

Here’s what happens:

  • Python opens the file

  • The code inside the with block runs

  • When the block ends, file.close() is called automatically, even if an error occurs

This makes the code safer and more readable.

36.3 What’s Happening Behind the Scenes?

The object after with must implement two special methods:

  • __enter__() — runs at the start (sets up the resource)

  • __exit__() — runs at the end (cleans up the resource)

Objects that implement these methods are called context managers.

open() returns a file object that is a context manager.

36.4 The with Statement Works with Many Resources

Although it’s commonly used with files, with can manage any resource that needs setup and teardown. Examples include:

  • managing locks (threading.Lock())

  • opening network connections

  • database sessions

  • temporary file operations

  • redirecting output

  • handling transactions

Example with a lock:

Show/Hide Code
from threading import Lock

lock = Lock()

with lock:
    # safe critical section
    print("Locked safely")
Locked safely

The lock is always released automatically.

36.5 Benefits of Using with

  • Cleaner code

  • Less error-prone

  • Automatic resource management

  • Built-in safety for exceptions

  • Easier to read and understand

Whenever you use resources that require cleanup, prefer the with statement.

37 How to Install 3rd-Party Packages in Python Using pip

Python comes with a powerful standard library that gives us many built-in tools. However, real-world projects usually need more specialized functionality, like data analysis, web development, machine learning, or working with APIs.

To bridge this gap, Python developers around the world create 3rd party packages. These packages are shared publicly so everyone can benefit from them.

All these packages are stored in one place: The Python Package Index (PyPI) https://pypi.org

At the time of writing, PyPI hosts over 270,000 packages, and you can install any of them using pip, Python’s package manager.

37.1 What is pip?

pip stands for Pip Installs Packages.

It is installed automatically when you install Python (as long as you used the official installer).

You can check if pip is installed by running:

pip --version

or

python -m pip --version

37.2 Installing a Package

To install a package from PyPI, use:

pip install <package>

For example, installing the popular HTTP library requests:

pip install requests

After installation, you can import and use it in any Python script:

import requests

37.3 If pip install gives errors

Sometimes you might have multiple Python versions installed. If so, run:

python -m pip install <package>

This ensures you’re using the correct Python interpreter.

37.4 Where are packages installed?

Packages are installed globally in a folder like:

  • Windows:
    C:\Users\<username>\AppData\Local\Programs\Python\Python39\Lib\site-packages

  • macOS (Python 3.9 example):
    /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages

  • Linux:
    /usr/lib/python3/dist-packages
    or inside your user folder.

37.5 Upgrading a Package

To update a package to its latest version:

pip install -U <package>

Example:

pip install -U pandas

37.6 Installing a Specific Version

Sometimes you need an older or exact version:

pip install <package>==<version>

Example:

pip install numpy==1.24.2

37.7 Uninstalling a Package

To remove a package completely:

pip uninstall <package>

Example:

pip uninstall requests

37.8 Viewing Package Information

To see the installed version, author, documentation URL, and installation path:

pip show <package>

Example:

pip show requests

This is especially useful when debugging version issues.

37.9 Listing All Installed Packages

You can see all installed libraries with:

pip list

37.10 Summary

  • Python uses pip to install 3rd-party packages.

  • Packages come from PyPI, a massive open-source repository.

  • You can install, upgrade, remove, or inspect packages easily using the commands shown in this c.

  • Installed packages can be imported in any Python script.

38 List Comprehensions in Python

In Python, a list comprehension is a concise and elegant way to create new lists.
Instead of writing multiple lines using loops, you can generate lists in a single, readable line.

Basic Example

Suppose you have a list:

Show/Hide Code
numbers = [1, 2, 3, 4, 5]

You can create a new list where each number is squared using a list comprehension:

Show/Hide Code
numbers_power_2 = [n**2 for n in numbers]

This does the same thing as a for loop but in a shorter, clearer way.

Why Use List Comprehensions?

List comprehensions are:

  • Shorter than loops

  • More readable when the logic fits on one line

  • Often faster than using loops

  • More Pythonic

Equivalent Using a Loop

Here is the same operation written as a normal loop:

Show/Hide Code
numbers_power_2 = []
for n in numbers:
    numbers_power_2.append(n**2)

Both work, but the list comprehension is cleaner.

Equivalent Using map() and lambda

You can also achieve the same using map():

Show/Hide Code
numbers_power_2 = list(map(lambda n: n**2, numbers))

This works but is usually less readable than a list comprehension.

Additional Examples

Example 1: Filter with a Condition

Get all even numbers:

Show/Hide Code
evens = [n for n in numbers if n % 2 == 0]

Example 2: Convert Strings to Uppercase

Show/Hide Code
words = ["hello", "python", "world"]
uppercased = [w.upper() for w in words]

Example 3: Create a List of Tuples

Show/Hide Code
pairs = [(n, n**2) for n in numbers]

Example 4: Nested Loops in a Comprehension

Show/Hide Code
matrix = [[i*j for j in range(3)] for i in range(3)]

General Syntax

[expression for item in iterable if condition]

Where:

  • expression → what to put in the new list

  • item → variable used inside the comprehension

  • iterable → existing list, string, or range

  • condition → optional filter

39 Polymorphism in Python

Polymorphism is an important concept in object-oriented programming (OOP).
The word means “many forms”, and in Python it allows the same function or method name to work on different types of objects.

In simple terms:

Different objects can respond to the same method name in different ways.

39.1 Basic Example of Polymorphism

We can define two different classes, and each class can have a method with the same name:

Show/Hide Code
class Dog:
    def eat(self):
        print('Eating dog food')

class Cat:
    def eat(self):
        print('Eating cat food')

Now, when we create objects:

Show/Hide Code
animal1 = Dog()
animal2 = Cat()

animal1.eat()   # Eating dog food
animal2.eat()   # Eating cat food
Eating dog food
Eating cat food

Even though both objects have an eat() method, each one behaves differently.

This allows us to write code that doesn’t need to care whether the object is a Dog or a Cat, it just calls .eat() and Python handles the rest.

Why Polymorphism Matters

  • Polymorphism helps you:

  • Write more flexible and extensible code

  • Avoid large if…else or switch statements

  • Work at a higher level of abstraction

  • Design functions that works with many types

A Common Real-World Example: len()

Python’s built-in len() function is polymorphic.

Show/Hide Code
len("hello")
len([1, 2, 3])
len({"a": 1})
1

len() works on strings, lists, dictionaries, and more — even though they are completely different types.

This is also polymorphism.

39.2 Polymorphism with Inheritance

In OOP, polymorphism often appears when subclasses override the same parent method.

Example:

Show/Hide Code
class Animal:
    def sound(self):
        print("Some sound")

class Dog(Animal):
    def sound(self):
        print("Woof!")

class Cat(Animal):
    def sound(self):
        print("Meow!")
Show/Hide Code
for animal in [Dog(), Cat(), Animal()]:
    animal.sound()
Woof!
Meow!
Some sound

Each object responds in its own way, that is polymorphism.

Summary

Polymorphism allows:

  • Same method name, different behaviors

  • Code that works with different objects interchangeably

  • Cleaner and more reusable design

It is one of the core features of object-oriented programming in Python.

40 Operator Overloading in Python

Python allows you to redefine how operators like +, >, ==, *, and many others behave when used on objects of your own classes.
This powerful feature is called operator overloading.

Operator overloading lets your custom objects behave more like built-in types (integers, strings, lists, etc.).

Basic Example

Consider a simple Dog class:

Show/Hide Code
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Let’s create two Dog objects:

Show/Hide Code
Max = Dog('Max', 8)
syd = Dog('Syd', 7)

Normally, comparing them does not work:

Show/Hide Code
print(Max > syd)  #TypeError

You must tell Python how to compare them. We can overload the greater than operator (>) by implementing __gt__():

Show/Hide Code
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __gt__(self, other):
        return self.age > other.age

Now this works:

Show/Hide Code
print(Max > syd)  # True

We have customized how > behaves for the Dog class.

40.1 Comparison Operator Methods

Python calls special methods (sometimes called “dunder” methods — double underscore) when you use operators.

Here are the most common comparison overloads:

Operator Method Meaning
== __eq__(self, other) Equal to
!= __ne__(self, other) Not equal
< __lt__(self, other) Less than
<= __le__(self, other) Less or equal
> __gt__(self, other) Greater than
>= __ge__(self, other) Greater or equal

40.2 Arithmetic Operator Overloading

You can also make your objects work with arithmetic operators:

Operator Method
+ __add__(self, other)
- __sub__(self, other)
* __mul__(self, other)
/ __truediv__(self, other)
// __floordiv__(self, other)
% __mod__(self, other)
** __pow__(self, other)

Example: Add the ages of two dogs

Show/Hide Code
def __add__(self, other):
    return self.age + other.age

Now:

Show/Hide Code
print(Max + syd)  # 15

Bitwise and Shift Operators

Your classes can also respond to operators like:

>>rshift()

<<lshift()

&and()

|or()

^xor()

These are less common unless you are working with binary or numeric data structures.

40.3 Why Operator Overloading Matters

It helps you:

  • Make your custom objects behave naturally with operator

  • Write cleaner and more intuitive code

  • Create classes that feel like built-in Python types

  • Encapsulate logic inside the class instead of writing separate functions everywhere

Examples in real-world libraries:

  • datetime uses + to add timedeltas

  • NumPy overloads *, +, -, etc. for arrays

  • Django models overload operators for database queries

40.4 Summary

Operator overloading lets you:

  • Redefine what operators mean for your objects

  • Implement comparison, arithmetic, logical, and bitwise behavior

  • Make class instances more powerful, readable, and pythonic

41 Virtual Environments in Python

When building Python applications, you will often encounter a situation where:

  • One project needs version 1 of a package

  • Another project needs version 2 of the same package

If all your packages are installed globally, these projects will conflict.

To avoid this problem, Python provides virtual environments, isolated “containers” that hold their own independent dependencies.

This ensures each project has its own clean and controlled environment.

41.1 What is a Virtual Environment?

A virtual environment:

  • Acts like a mini Python installation inside your project

  • Has its own site-packages directory

  • Keeps dependencies separated from your system Python

  • Prevents version conflicts between projects

41.2 Creating a Virtual Environment

Python comes with a built-in tool called venv.

Inside your project folder, run:

python -m venv .venv 

This creates a hidden directory named .venv that contains the entire environment.

You will see folders inside it like:

  • bin/ (or Scripts/ on Windows)

  • lib/

  • pyvenv.cfg

41.3 Activating a Virtual Environment

Once created, activate it:

On macOS & Linux

source .venv/bin/activate 

On Fish shell

source .venv/bin/activate.fish 

On Windows (PowerShell)

.\.venv\Scripts\Activate.ps1 

After activation, your terminal prompt usually changes.
For example:

Before:

 myproject 

After:

(.venv)  myproject 

This indicates you are inside the virtual environment.

41.4 Using pip Inside a Virtual Environment

Once activated, pip automatically installs packages into this environment only, not globally.

Example:

pip install requests 

The installed package stays inside .venv, fully isolated from your system.

Your global Python remains clean and untouched.

Deactivating a Virtual Environment

To exit the virtual environment, simply run:

deactivate 

Your terminal returns to normal, and you’re back to using the system Python.

41.5 Why Virtual Environments Are Essential

Using virtual environments solves many problems:

✔ Avoids version conflicts
✔ Keeps projects lightweight and reproducible
✔ Makes collaboration easier
✔ Matches production environments more reliably
✔ Keeps your system Python clean

Almost every professional Python developer uses virtual environments for every project.

41.6 Alternative Tools

Besides venv, other environment managers exist:

  • pipenv — combines virtualenv + dependency management

  • poetry — powerful dependency and project manager

  • conda — environment and package manager (used in data science)

But venv remains the simplest and is included in Python by default.

42 Conclusion

Python is a powerful, flexible, and expressive programming language that continues to grow in popularity across fields such as web development, data science, automation, machine learning, and more.

Throughout this training, we covered a wide range of topics, from basic syntax to advanced concepts, designed to give you a strong foundation and the confidence to build real applications. You learned how Python handles variables, loops, functions, classes, exceptions, modules, virtual environments, and many more features that make it one of the most enjoyable languages to work with.

As you continue your Python journey, remember:

  • Practice is essential. The more you code, the more the concepts will become second nature.

  • Read other people’s code. It helps you discover new patterns and techniques.

  • Explore Python’s ecosystem. With hundreds of thousands of packages available, Python becomes even more powerful.

  • Continue learning. The language evolves, and so will your skills.

With this foundation, you are now well-prepared to move into more advanced areas such as data analysis, machine learning, automation, web development, or any other Python-driven field.

Congratulations on completing the course, and keep building!