Friday, May 29, 2026

PYTHON PROGRAMMING: A COMPLETE GUIDE - From Your First Line of Code to Advanced Techniques




INTRODUCTION: WELCOME TO THE WORLD OF PYTHON


Python is one of the most popular and beginner-friendly programming languages in the world today. Created by Guido van Rossum and first released in 1991, Python was designed with a philosophy that emphasizes code readability and simplicity. The name Python comes from the British comedy group Monty Python, reflecting the language's focus on making programming fun and accessible.

What makes Python special is its clean syntax that reads almost like English. Unlike many other programming languages that require semicolons, curly braces, and complex punctuation, Python uses indentation to define code blocks. This makes Python code naturally organized and easy to read, even for complete beginners.

Python is used everywhere in the modern world. Web developers use it to build websites with frameworks like Django and Flask. Data scientists rely on Python for analyzing massive datasets and creating visualizations. Machine learning engineers use Python libraries like TensorFlow and PyTorch to build artificial intelligence systems. System administrators write Python scripts to automate repetitive tasks. The versatility of Python means that learning it opens doors to countless career opportunities.

In this tutorial, we will start from the very beginning, assuming you have never written a single line of code before. We will progress step by step through the fundamentals, building your understanding gradually. By the end of this tutorial, you will have the knowledge and confidence to write your own Python programs and continue learning on your own.


PART ONE: GETTING STARTED WITH PYTHON


CHAPTER ONE: INSTALLING PYTHON AND YOUR FIRST PROGRAM


Before you can write Python code, you need to install Python on your computer. Visit the official Python website at python.org and download the latest version of Python 3. During installation, make sure to check the box that says "Add Python to PATH" as this will make running Python much easier.

Once Python is installed, you can verify it works by opening a command prompt or terminal and typing "python --version". You should see the version number displayed.

Now let's write your very first Python program. Open a text editor and type the following:


print("Hello, World!")

Save this file as "hello.py" and run it by opening a command prompt in the same directory and typing "python hello.py". You should see the text 

"Hello, World!

appear on your screen.


Congratulations! You have just written and executed your first Python program. The print function is one of the most fundamental functions in Python. It takes whatever you put inside the parentheses and displays it on the screen. The text inside quotation marks is called a string, which is how we represent text in programming.


CHAPTER TWO: VARIABLES AND DATA TYPES


In programming, we need ways to store and manipulate information. Variables are like labeled boxes where we can store data. In Python, creating a variable is incredibly simple. You just write a name, an equals sign, and the value you want to store.

age = 25
name = "Alice"
height = 5.8
is_student = True

In this example, we created four different variables. The variable "age" stores the number 25. The variable "name" stores the text "Alice". The variable "height" stores the decimal number 5.8. The variable "is_student" stores the boolean value True.

Python has several built-in data types that you need to understand. Integers are whole numbers without decimal points, like 42, negative 17, or 0. Floats are numbers with decimal points, like 3.14 or negative 0.5. Strings are sequences of characters enclosed in quotes, like "Hello" or "Python is fun". Booleans represent truth values and can only be True or False.


You can check what type a variable is using the type function:

my_number = 42
print(type(my_number))  # This will print: <class 'int'>

my_text = "Hello"
print(type(my_text))  # This will print: <class 'str'>


One powerful feature of Python is that variables can change their type. This is called dynamic typing:


value = 10  # value is an integer
print(value)

value = "Now I'm text"  # value is now a string
print(value)

This flexibility makes Python very convenient, though you should be careful to keep track of what type your variables are to avoid confusion.

CHAPTER THREE: BASIC OPERATIONS AND EXPRESSIONS

Python can perform all the mathematical operations you would expect. Addition uses the plus sign, subtraction uses the minus sign, multiplication uses the asterisk, and division uses the forward slash.


# Basic arithmetic operations
sum_result = 10 + 5  # Addition: 15
difference = 10 - 5  # Subtraction: 5
product = 10 * 5  # Multiplication: 50
quotient = 10 / 5  # Division: 2.0

print("Sum:", sum_result)
print("Difference:", difference)
print("Product:", product)
print("Quotient:", quotient)

Notice that division always returns a float, even when dividing evenly. If you want integer division that rounds down, use two forward slashes:


integer_division = 10 // 3  # Result: 3
print(integer_division)

The modulo operator uses the percent sign and gives you the remainder after division:


remainder = 10 % 3  # Result: 1
print(remainder)

Exponentiation uses two asterisks:


power = 2 ** 3  # Two to the power of three: 8
print(power)

You can combine operations, and Python follows the standard order of operations that you learned in mathematics. Parentheses have the highest priority, then exponentiation, then multiplication and division, and finally addition and subtraction.


complex_calculation = (10 + 5) * 2 ** 2 - 8 / 2
# First: (10 + 5) = 15
# Then: 2 ** 2 = 4
# Then: 15 * 4 = 60
# Then: 8 / 2 = 4.0
# Finally: 60 - 4.0 = 56.0
print(complex_calculation)

Strings also support some operations. You can concatenate strings using the plus operator:


first_name = "John"
last_name = "Smith"
full_name = first_name + " " + last_name
print(full_name)  # Output: John Smith


You can repeat strings using the multiplication operator:


repeated = "Ha" * 3
print(repeated)  # Output: HaHaHa


CHAPTER FOUR: GETTING INPUT FROM USERS


Programs become much more interesting when they can interact with users. The input function allows your program to receive text from the user:


# Getting user input
user_name = input("What is your name? ")
print("Hello, " + user_name + "! Nice to meet you.")

When this program runs, it will display the question "What is your name?" and wait for the user to type something and press Enter. Whatever the user types gets stored in the variable "user_name".

One important thing to understand is that the input function always returns a string, even if the user types a number. If you want to use the input as a number, you need to convert it:


# Converting string input to integer
age_string = input("How old are you? ")
age_number = int(age_string)


# Now we can do math with it
years_until_hundred = 100 - age_number
print("You have", years_until_hundred, "years until you turn 100.")



The int function converts a string to an integer. Similarly, float converts to a decimal number, and str converts to a string. Be careful though, because if the user types something that cannot be converted, your program will crash with an error. We will learn how to handle such errors later in this tutorial.


CHAPTER FIVE: MAKING DECISIONS WITH IF STATEMENTS


Programs need to make decisions based on conditions. The if statement allows your code to execute different actions depending on whether something is true or false.


# Simple if statement
temperature = 25

if temperature > 30:
    print("It's hot outside!")
    print("Remember to drink water.")

In this example, the code inside the if block only runs if the temperature is greater than 30. Notice the indentation. In Python, indentation is not just for readability, it actually defines which code belongs to the if statement. Everything indented under the if line is part of the conditional block.

You can add an else clause to specify what should happen when the condition is false:


temperature = 15

if temperature > 30:
    print("It's hot outside!")
else:
    print("The weather is pleasant.")

For multiple conditions, use elif, which is short for "else if":


temperature = 25

if temperature > 30:
    print("It's hot outside!")
elif temperature > 20:
    print("The weather is nice.")
elif temperature > 10:
    print("It's a bit cool.")
else:
    print("It's cold! Wear a jacket.")


Python checks each condition in order and executes the first block where the condition is true, then skips the rest.

You can combine conditions using logical operators. The "and" operator requires both conditions to be true. The "or" operator requires at least one condition to be true. The "not" operator reverses a boolean value.


age = 25
has_license = True

if age >= 18 and has_license:
    print("You can drive a car.")
else:
    print("You cannot drive a car.")

is_weekend = False
is_holiday = True

if is_weekend or is_holiday:
    print("No work today!")
else:
    print("Time to go to work.")

Comparison operators allow you to compare values. The double equals sign checks if two values are equal. The exclamation mark followed by an equals sign checks if they are not equal. You also have greater than, less than, greater than or equal to, and less than or equal to.


x = 10
y = 20

print(x == y)  # False: x is not equal to y
print(x != y)  # True: x is not equal to y
print(x < y)   # True: x is less than y
print(x > y)   # False: x is not greater than y
print(x <= 10) # True: x is less than or equal to 10
print(x >= 10) # True: x is greater than or equal to 10



PART TWO: CONTROL FLOW AND REPETITION


CHAPTER SIX: WHILE LOOPS


Often you need to repeat an action multiple times. The while loop continues executing a block of code as long as a condition remains true.


# Counting from 1 to 5
counter = 1

while counter <= 5:
    print("Count:", counter)
    counter = counter + 1

print("Finished counting!")

This program starts with counter equal to 1. It checks if counter is less than or equal to 5. Since this is true, it prints the counter and increases it by 1. This repeats until counter becomes 6, at which point the condition becomes false and the loop stops.

Be very careful with while loops. If the condition never becomes false, you create an infinite loop that runs forever. For example, if you forgot to increase the counter in the previous example, the loop would never end.

You can use the break statement to exit a loop early:


# Keep asking until user enters 'quit'
while True:
    user_input = input("Enter a command (or 'quit' to exit): ")
    
    if user_input == "quit":
        print("Goodbye!")
        break
    
    print("You entered:", user_input)


The continue statement skips the rest of the current iteration and goes to the next one:


# Print odd numbers from 1 to 10
number = 0

while number < 10:
    number = number + 1
    
    if number % 2 == 0:
        continue  # Skip even numbers
    
    print(number)


CHAPTER SEVEN: FOR LOOPS AND RANGES


The for loop is used to iterate over a sequence of items. The most common way to use it is with the range function, which generates a sequence of numbers.


# Print numbers from 0 to 4
for i in range(5):
    print(i)


The range function with one argument generates numbers starting from 0 up to but not including the argument. So range(5) generates 0, 1, 2, 3, 4.

You can specify a starting point and an ending point:


# Print numbers from 2 to 6
for i in range(2, 7):
    print(i)

You can also specify a step size:


# Print even numbers from 0 to 10
for i in range(0, 11, 2):
    print(i)


For loops are generally preferred over while loops when you know in advance how many times you want to repeat something. They are cleaner and less prone to infinite loop errors.

You can use for loops to iterate over strings:


# Print each character in a word
word = "Python"

for letter in word:
    print(letter)

The break and continue statements also work in for loops:


# Find the first number divisible by 7
for number in range(1, 100):
    if number % 7 == 0:
        print("First number divisible by 7:", number)
        break

CHAPTER EIGHT: NESTED LOOPS


You can place loops inside other loops. This is called nesting and is useful for working with two-dimensional data or creating patterns.


# Multiplication table
for i in range(1, 6):
    for j in range(1, 6):
        product = i * j
        print(i, "x", j, "=", product)
    print()  # Empty line after each row

In this example, the outer loop runs 5 times with i going from 1 to 5. For each value of i, the inner loop runs 5 times with j going from 1 to 5. This creates a complete multiplication table.

Here is another example that creates a simple pattern:


# Print a triangle pattern
rows = 5

for i in range(1, rows + 1):
    for j in range(i):
        print("*", end="")
    print()  # Move to next line


The "end" parameter in the print function controls what gets printed at the end. By default it is a newline, but here we change it to an empty string so the asterisks print on the same line.


PART THREE: DATA STRUCTURES


CHAPTER NINE: LISTS


Lists are one of the most important data structures in Python. A list is an ordered collection of items that can be of any type. You create a list using square brackets:


# Creating a list of numbers
numbers = [1, 2, 3, 4, 5]
print(numbers)

# Creating a list of strings
fruits = ["apple", "banana", "orange"]
print(fruits)

# Lists can contain mixed types
mixed = [1, "hello", 3.14, True]
print(mixed)


You access individual items in a list using their index, which starts at 0:


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

print(fruits[0])  # First item: apple
print(fruits[1])  # Second item: banana
print(fruits[2])  # Third item: orange


Negative indices count from the end of the list:


print(fruits[-1])  # Last item: orange
print(fruits[-2])  # Second to last: banana


You can modify items in a list:


fruits = ["apple", "banana", "orange"]
fruits[1] = "grape"
print(fruits)  # Output: ['apple', 'grape', 'orange']


Lists have many useful methods. The append method adds an item to the end:


fruits = ["apple", "banana"]
fruits.append("orange")
print(fruits)  # Output: ['apple', 'banana', 'orange']


The insert method adds an item at a specific position:


fruits = ["apple", "orange"]
fruits.insert(1, "banana")  # Insert at index 1
print(fruits)  # Output: ['apple', 'banana', 'orange']

The remove method deletes the first occurrence of a value:


fruits = ["apple", "banana", "orange"]
fruits.remove("banana")
print(fruits)  # Output: ['apple', 'orange']

The pop method removes and returns an item at a specific index:


fruits = ["apple", "banana", "orange"]
removed_fruit = fruits.pop(1)
print("Removed:", removed_fruit)  # Output: Removed: banana
print(fruits)  # Output: ['apple', 'orange']

You can check if an item exists in a list using the "in" operator:


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

if "banana" in fruits:
    print("We have bananas!")


The len function returns the number of items in a list:


fruits = ["apple", "banana", "orange"]
print("Number of fruits:", len(fruits))  # Output: 3


You can iterate over a list using a for loop:


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

for fruit in fruits:
    print("I like", fruit)


List slicing allows you to get a portion of a list:


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

print(numbers[2:5])   # Items from index 2 to 4: [2, 3, 4]
print(numbers[:4])    # Items from start to index 3: [0, 1, 2, 3]
print(numbers[6:])    # Items from index 6 to end: [6, 7, 8, 9]
print(numbers[::2])   # Every second item: [0, 2, 4, 6, 8]
print(numbers[::-1])  # Reverse the list: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]


CHAPTER TEN: TUPLES


Tuples are similar to lists but with one crucial difference: they are immutable, meaning once created, they cannot be modified. You create tuples using parentheses:


# Creating a tuple
coordinates = (10, 20)
print(coordinates)

# Tuple with mixed types
person = ("Alice", 25, "Engineer")
print(person)

You access tuple items the same way as lists:


coordinates = (10, 20)
print(coordinates[0])  # Output: 10
print(coordinates[1])  # Output: 20


However, you cannot modify tuple items:


coordinates = (10, 20)
# coordinates[0] = 15  # This would cause an error!


Tuples are useful when you want to ensure that data cannot be accidentally changed. They are also slightly more efficient than lists in terms of memory and performance.

You can unpack tuples into separate variables:


coordinates = (10, 20)
x, y = coordinates
print("x:", x)  # Output: x: 10
print("y:", y)  # Output: y: 20


This unpacking feature is very convenient and is often used when functions return multiple values.


CHAPTER ELEVEN: DICTIONARIES


Dictionaries store data as key-value pairs. Instead of accessing items by numeric index, you access them by their key. You create dictionaries using curly braces:


# Creating a dictionary
student = {
    "name": "Alice",
    "age": 20,
    "major": "Computer Science"
}

print(student)

You access values using their keys:


print(student["name"])   # Output: Alice
print(student["age"])    # Output: 20
print(student["major"])  # Output: Computer Science

You can add new key-value pairs or modify existing ones:


student = {"name": "Alice", "age": 20}

# Add a new key-value pair
student["major"] = "Computer Science"

# Modify an existing value
student["age"] = 21

print(student)

The get method provides a safe way to access values, returning None if the key does not exist:


student = {"name": "Alice", "age": 20}

print(student.get("name"))    # Output: Alice
print(student.get("major"))   # Output: None

# You can provide a default value
print(student.get("major", "Undecided"))  # Output: Undecided


You can check if a key exists using the "in" operator:

student = {"name": "Alice", "age": 20}

if "name" in student:
    print("Name:", student["name"])

The keys method returns all keys, and the values method returns all values:


student = {"name": "Alice", "age": 20, "major": "CS"}

print(student.keys())    # Output: dict_keys(['name', 'age', 'major'])
print(student.values())  # Output: dict_values(['Alice', 20, 'CS'])


You can iterate over dictionaries:


student = {"name": "Alice", "age": 20, "major": "CS"}

# Iterate over keys
for key in student:
    print(key, ":", student[key])

# Iterate over key-value pairs
for key, value in student.items():
    print(key, ":", value)

Dictionaries are incredibly useful for organizing related data and are one of Python's most powerful features.


CHAPTER TWELVE: SETS


Sets are unordered collections of unique items. They automatically remove duplicates and are useful for membership testing and mathematical set operations. You create sets using curly braces or the set function:


# Creating a set
fruits = {"apple", "banana", "orange"}
print(fruits)

# Creating a set from a list (duplicates removed)
numbers = [1, 2, 2, 3, 3, 3, 4]
unique_numbers = set(numbers)
print(unique_numbers)  # Output: {1, 2, 3, 4}


You can add items to a set:


fruits = {"apple", "banana"}
fruits.add("orange")
print(fruits)

You can remove items:


fruits = {"apple", "banana", "orange"}
fruits.remove("banana")
print(fruits)

Sets support mathematical operations like union, intersection, and difference:


set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}

# Union: all items from both sets
print(set_a | set_b)  # Output: {1, 2, 3, 4, 5, 6}

# Intersection: items in both sets
print(set_a & set_b)  # Output: {3, 4}

# Difference: items in set_a but not in set_b
print(set_a - set_b)  # Output: {1, 2}


Sets are very fast for checking membership:

large_set = set(range(1000000))

if 500000 in large_set:
    print("Found it!")

PART FOUR: FUNCTIONS AND MODULARITY


CHAPTER THIRTEEN: DEFINING FUNCTIONS


Functions are reusable blocks of code that perform specific tasks. They help you organize your code and avoid repetition. You define a function using the "def" keyword:


# Simple function that prints a greeting
def greet():
    print("Hello, welcome to Python!")

# Call the function
greet()


Functions can accept parameters, which are values you pass to the function:


# Function with a parameter
def greet_person(name):
    print("Hello,", name, "!")

# Call the function with different arguments
greet_person("Alice")
greet_person("Bob")

Functions can have multiple parameters:


# Function with multiple parameters
def introduce(name, age, city):
    print("My name is", name)
    print("I am", age, "years old")
    print("I live in", city)


introduce("Alice", 25, "New York")

Functions can return values using the return statement:


# Function that calculates the square of a number
def square(number):
    result = number * number
    return result

# Use the returned value
answer = square(5)
print("The square of 5 is", answer)


You can return multiple values as a tuple:


# Function that returns multiple values
def calculate_rectangle(width, height):
    area = width * height
    perimeter = 2 * (width + height)
    return area, perimeter

# Unpack the returned values
rect_area, rect_perimeter = calculate_rectangle(5, 3)
print("Area:", rect_area)
print("Perimeter:", rect_perimeter)


Functions can have default parameter values:


# Function with default parameter
def greet_with_title(name, title="Mr."):
    print("Hello,", title, name)

greet_with_title("Smith")           # Uses default: Hello, Mr. Smith
greet_with_title("Johnson", "Dr.")  # Uses provided: Hello, Dr. Johnson


You can use keyword arguments to make function calls more readable:


def create_profile(name, age, city, occupation):
    print("Name:", name)
    print("Age:", age)
    print("City:", city)
    print("Occupation:", occupation)

# Call with keyword arguments (order doesn't matter)
create_profile(city="Boston", name="Alice", occupation="Engineer", age=28)


CHAPTER FOURTEEN: VARIABLE SCOPE


Variables have scope, which determines where they can be accessed. Variables defined inside a function are local to that function and cannot be accessed outside:


# Local variable example
def my_function():
    local_var = 10  # This variable only exists inside the function
    print("Inside function:", local_var)

my_function()
# print(local_var)  # This would cause an error!

Variables defined outside functions are global and can be accessed anywhere:


# Global variable example
global_var = 20

def my_function():
    print("Inside function:", global_var)  # Can access global variable

my_function()
print("Outside function:", global_var)

If you want to modify a global variable inside a function, you need to use the global keyword:


# Modifying global variable
counter = 0

def increment():
    global counter
    counter = counter + 1

increment()
increment()
print("Counter:", counter)  # Output: Counter: 2


However, using global variables is generally discouraged because it can make code harder to understand and debug. It is better to pass values as parameters and return results.


CHAPTER FIFTEEN: LAMBDA FUNCTIONS


Lambda functions are small anonymous functions defined using the lambda keyword. They are useful for simple operations that you need to use only once:


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

# Equivalent lambda function
square_lambda = lambda x: x * x

print(square(5))         # Output: 25
print(square_lambda(5))  # Output: 25


Lambda functions are often used with functions like map, filter, and sorted:


# Using lambda with map to apply a function to each item
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x * x, numbers))
print(squared)  # Output: [1, 4, 9, 16, 25]

# Using lambda with filter to select items
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)  # Output: [2, 4, 6, 8, 10]

# Using lambda with sorted for custom sorting
students = [
    {"name": "Alice", "grade": 85},
    {"name": "Bob", "grade": 92},
    {"name": "Charlie", "grade": 78}
]
sorted_students = sorted(students, key=lambda s: s["grade"], reverse=True)
for student in sorted_students:
    print(student["name"], ":", student["grade"])


CHAPTER SIXTEEN: MODULES AND IMPORTS


As your programs grow larger, you will want to organize code into separate files called modules. Python comes with many built-in modules that provide additional functionality.

To use a module, you import it:


# Importing the math module
import math

# Using functions from the math module
print(math.sqrt(16))      # Square root: 4.0
print(math.pi)            # Pi constant: 3.14159...
print(math.sin(math.pi))  # Sine function: 0.0

You can import specific items from a module:

# Import only specific items
from math import sqrt, pi

print(sqrt(25))  # No need to write math.sqrt
print(pi)

You can give imports an alias:


# Import with an alias
import math as m

print(m.sqrt(9))


Some commonly used built-in modules include math for mathematical functions, random for generating random numbers, datetime for working with dates and times, and os for interacting with the operating system.

Here is an example using the random module:


import random

# Generate a random integer between 1 and 10
random_number = random.randint(1, 10)
print("Random number:", random_number)

# Choose a random item from a list
fruits = ["apple", "banana", "orange"]
random_fruit = random.choice(fruits)
print("Random fruit:", random_fruit)

# Shuffle a list
numbers = [1, 2, 3, 4, 5]
random.shuffle(numbers)
print("Shuffled:", numbers)

You can create your own modules by saving Python code in a file and importing it. For example, if you have a file called "mymodule.py" with this content:


def greet(name):
    return "Hello, " + name

You can import and use it in another file:


import mymodule

message = mymodule.greet("Alice")
print(message)


PART FIVE: WORKING WITH FILES


CHAPTER SEVENTEEN: READING FILES


Python makes it easy to work with files. To read a file, you use the open function with the mode "r" for reading:


# Reading an entire file
file = open("example.txt", "r")
content = file.read()
print(content)
file.close()


It is important to close files after you are done with them. A better approach is to use the "with" statement, which automatically closes the file:


# Reading a file with the with statement
with open("example.txt", "r") as file:
    content = file.read()
    print(content)
# File is automatically closed here

You can read a file line by line:

# Reading line by line
with open("example.txt", "r") as file:
    for line in file:
        print(line.strip())  # strip() removes the newline character


You can read all lines into a list:


# Reading all lines into a list
with open("example.txt", "r") as file:
    lines = file.readlines()
    for line in lines:
        print(line.strip())

CHAPTER EIGHTEEN: WRITING FILES


To write to a file, use the mode "w" for writing. Be careful because this will overwrite the file if it already exists:


# Writing to a file
with open("output.txt", "w") as file:
    file.write("Hello, World!\n")
    file.write("This is a new line.\n")

To append to a file without overwriting it, use the mode "a":


# Appending to a file
with open("output.txt", "a") as file:
    file.write("This line is appended.\n")

You can write multiple lines at once:

# Writing multiple lines
lines = ["First line\n", "Second line\n", "Third line\n"]

with open("output.txt", "w") as file:
    file.writelines(lines)


CHAPTER NINETEEN: WORKING WITH CSV FILES


CSV (Comma-Separated Values) files are commonly used for storing tabular data. Python has a built-in csv module for working with them:

import csv

# Writing to a CSV file
data = [
    ["Name", "Age", "City"],
    ["Alice", "25", "New York"],
    ["Bob", "30", "Los Angeles"],
    ["Charlie", "35", "Chicago"]
]

with open("people.csv", "w", newline="") as file:
    writer = csv.writer(file)
    writer.writerows(data)

# Reading from a CSV file
with open("people.csv", "r") as file:
    reader = csv.reader(file)
    for row in reader:
        print(row)


You can also work with CSV files using dictionaries:


import csv

# Writing dictionaries to CSV
data = [
    {"Name": "Alice", "Age": 25, "City": "New York"},
    {"Name": "Bob", "Age": 30, "City": "Los Angeles"}
]

with open("people.csv", "w", newline="") as file:
    fieldnames = ["Name", "Age", "City"]
    writer = csv.DictWriter(file, fieldnames=fieldnames)
    writer.writeheader()
    writer.writerows(data)

# Reading CSV into dictionaries
with open("people.csv", "r") as file:
    reader = csv.DictReader(file)
    for row in reader:
        print(row["Name"], "is", row["Age"], "years old")


PART SIX: OBJECT-ORIENTED PROGRAMMING


CHAPTER TWENTY: CLASSES AND OBJECTS


Object-oriented programming is a programming paradigm that organizes code around objects, which are instances of classes. A class is like a blueprint that defines the properties and behaviors of objects.

Here is a simple class definition:


# Defining a class
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def bark(self):
        print(self.name, "says Woof!")
    
    def get_info(self):
        return self.name + " is " + str(self.age) + " years old"

# Creating objects (instances) of the class
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

# Using object methods
dog1.bark()  # Output: Buddy says Woof!
print(dog1.get_info())  
# Output: Buddy is years old dog2.bark() # Output: Max says Woof! print(dog2.get_info()) # Output: Max is 5 years old


The __init__ method is a special method called a constructor that runs when you create a new object. The "self" parameter refers to the instance being created and allows you to set attributes specific to that instance.

Attributes are variables that belong to an object. In the example above, "name" and "age" are attributes. Methods are functions that belong to a class and define the behaviors of objects.


CHAPTER TWENTY-ONE: ENCAPSULATION AND PROPERTIES


Encapsulation is the principle of bundling data and methods that work on that data within a class. It also involves controlling access to the internal state of objects.

In Python, you can create private attributes by prefixing them with double underscores:


# Class with private attributes
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        if amount > 0:
            self.__balance = self.__balance + amount
            print("Deposited:", amount)
        else:
            print("Invalid deposit amount")
    
    def withdraw(self, amount):
        if amount > 0 and amount <= self.__balance:
            self.__balance = self.__balance - amount
            print("Withdrew:", amount)
        else:
            print("Invalid withdrawal amount")
    
    def get_balance(self):
        return self.__balance

# Using the class
account = BankAccount("Alice", 1000)
account.deposit(500)
account.withdraw(200)
print("Current balance:", account.get_balance())


The double underscore prefix makes the balance attribute private, meaning it should not be accessed directly from outside the class. Instead, you use methods to interact with it, which allows you to add validation and maintain control.

Python also supports properties, which allow you to use method calls like attribute access:


# Using properties
class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius
    
    @property
    def celsius(self):
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is not possible")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

# Using the class
temp = Temperature(25)
print("Celsius:", temp.celsius)
print("Fahrenheit:", temp.fahrenheit)

temp.fahrenheit = 98.6
print("New Celsius:", temp.celsius)

The @property decorator allows you to define a method that can be accessed like an attribute. The @setter decorator allows you to define how the attribute should be set.


CHAPTER TWENTY-TWO: INHERITANCE

Inheritance allows you to create new classes based on existing classes. The new class inherits attributes and methods from the parent class and can add or override them.


# Parent class
class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species
    
    def make_sound(self):
        print("Some generic animal sound")
    
    def get_info(self):
        return self.name + " is a " + self.species

# Child class inheriting from Animal
class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name, "Dog")  # Call parent constructor
        self.breed = breed
    
    def make_sound(self):  # Override parent method
        print("Woof! Woof!")
    
    def fetch(self):  # New method specific to Dog
        print(self.name, "is fetching the ball")

# Another child class
class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name, "Cat")
        self.color = color
    
    def make_sound(self):
        print("Meow!")
    
    def scratch(self):
        print(self.name, "is scratching the furniture")

# Using the classes
dog = Dog("Buddy", "Golden Retriever")
cat = Cat("Whiskers", "Orange")

print(dog.get_info())  # Inherited method
dog.make_sound()       # Overridden method
dog.fetch()            # New method

print(cat.get_info())
cat.make_sound()
cat.scratch()

The super() function is used to call methods from the parent class. This is especially useful in the constructor to initialize the parent class attributes.

Inheritance promotes code reuse and allows you to create hierarchies of related classes. You can have multiple levels of inheritance, where a child class becomes the parent of another class.


CHAPTER TWENTY-THREE: POLYMORPHISM


Polymorphism means "many forms" and refers to the ability of different classes to be used interchangeably through a common interface. In Python, this is achieved through method overriding and duck typing.


# Demonstrating polymorphism
class Shape:
    def area(self):
        pass
    
    def perimeter(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius * self.radius
    
    def perimeter(self):
        return 2 * 3.14159 * self.radius

# Function that works with any shape
def print_shape_info(shape):
    print("Area:", shape.area())
    print("Perimeter:", shape.perimeter())

# Using polymorphism
rectangle = Rectangle(5, 3)
circle = Circle(4)

print("Rectangle:")
print_shape_info(rectangle)

print("\nCircle:")
print_shape_info(circle)

The function print_shape_info works with any object that has area and perimeter methods, regardless of the specific class. This is polymorphism in action.

Python uses duck typing, which means "if it walks like a duck and quacks like a duck, it's a duck." In other words, Python cares about what methods an object has, not what class it belongs to.


CHAPTER TWENTY-FOUR: SPECIAL METHODS

Python classes can define special methods that allow objects to work with built-in Python operations. These methods have names that start and end with double underscores.


# Class with special methods
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        # Called by str() and print()
        return "Vector(" + str(self.x) + ", " + str(self.y) + ")"
    
    def __repr__(self):
        # Called by repr() and in interactive mode
        return "Vector(" + str(self.x) + ", " + str(self.y) + ")"
    
    def __add__(self, other):
        # Called by the + operator
        return Vector(self.x + other.x, self.y + other.y)
    
    def __sub__(self, other):
        # Called by the - operator
        return Vector(self.x - other.x, self.y - other.y)
    
    def __mul__(self, scalar):
        # Called by the * operator
        return Vector(self.x * scalar, self.y * scalar)
    
    def __eq__(self, other):
        # Called by the == operator
        return self.x == other.x and self.y == other.y
    
    def __len__(self):
        # Called by len()
        return 2
    
    def __getitem__(self, index):
        # Called by indexing: vector[0]
        if index == 0:
            return self.x
        elif index == 1:
            return self.y
        else:
            raise IndexError("Vector index out of range")


# Using the special methods
v1 = Vector(2, 3)
v2 = Vector(5, 7)

print(v1)  # Uses __str__

v3 = v1 + v2  # Uses __add__
print("Sum:", v3)

v4 = v2 - v1  # Uses __sub__
print("Difference:", v4)

v5 = v1 * 3  # Uses __mul__
print("Scaled:", v5)

print("Equal?", v1 == v2)  # Uses __eq__

print("Length:", len(v1))  # Uses __len__

print("First component:", v1[0])  # Uses __getitem__

These special methods make your custom classes behave like built-in types, which makes them more intuitive to use.


PART SEVEN: ERROR HANDLING AND EXCEPTIONS


CHAPTER TWENTY-FIVE: UNDERSTANDING EXCEPTIONS


When something goes wrong in a Python program, an exception is raised. If not handled, the exception causes the program to crash. Common exceptions include ValueError when you try to convert an invalid string to a number, ZeroDivisionError when you divide by zero, and FileNotFoundError when you try to open a file that does not exist.

Here is an example of an unhandled exception:


# This will crash
number = int("abc")  # ValueError: invalid literal for int()

To handle exceptions gracefully, you use try-except blocks:


# Handling an exception
try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print("Result:", result)
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("You cannot divide by zero!")


The code in the try block is executed. If an exception occurs, Python looks for a matching except block and executes it instead of crashing.

You can catch multiple exceptions in one except block:


try:
    number = int(input("Enter a number: "))
    result = 100 / number
    print("Result:", result)
except (ValueError, ZeroDivisionError) as error:
    print("An error occurred:", error)

You can have a generic except block that catches all exceptions:

try:
    # Some risky operation
    result = risky_function()
except Exception as error:
    print("Something went wrong:", error)

However, catching all exceptions is generally discouraged because it can hide bugs. It is better to catch specific exceptions that you expect might occur.


CHAPTER TWENTY-SIX: ELSE AND FINALLY CLAUSES


The try-except statement can have else and finally clauses that provide additional control.


The else clause runs if no exception occurred:


try:
    number = int(input("Enter a number: "))
except ValueError:
    print("Invalid input!")
else:
    print("You entered:", number)
    print("Its square is:", number * number)

The finally clause always runs, whether an exception occurred or not. This is useful for cleanup operations like closing files:


try:
    file = open("data.txt", "r")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("File not found!")
finally:
    # This always runs
    print("Cleanup complete")

A more practical example with file handling:


file = None
try:
    file = open("data.txt", "r")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("File not found!")
finally:
    if file is not None:
        file.close()
        print("File closed")


CHAPTER TWENTY-SEVEN: RAISING EXCEPTIONS

You can raise exceptions yourself using the raise statement. This is useful when you want to signal that something is wrong:


# Function that raises an exception
def calculate_age(birth_year):
    current_year = 2025
    age = current_year - birth_year
    
    if age < 0:
        raise ValueError("Birth year cannot be in the future")
    if age > 150:
        raise ValueError("Age seems unrealistic")
    
    return age

# Using the function
try:
    age = calculate_age(2030)
    print("Age:", age)
except ValueError as error:
    print("Error:", error)

You can create custom exception classes by inheriting from the Exception class:


# Custom exception class
class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        message = "Insufficient funds: balance is " + str(balance) + " but tried to withdraw " + str(amount)
        super().__init__(message)

# Using the custom exception
class BankAccount:
    def __init__(self, balance):
        self.balance = balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(self.balance, amount)
        self.balance = self.balance - amount
        return self.balance

# Testing the exception
account = BankAccount(100)
try:
    account.withdraw(150)
except InsufficientFundsError as error:
    print("Error:", error)
    print("Current balance:", error.balance)


PART EIGHT: ADVANCED TOPICS


CHAPTER TWENTY-EIGHT: LIST COMPREHENSIONS


List comprehensions provide a concise way to create lists. They are more readable and often faster than traditional loops.

# Traditional approach
squares = []
for i in range(10):
    squares.append(i * i)
print(squares)

# List comprehension approach
squares = [i * i for i in range(10)]
print(squares)


The basic syntax is: new_list = [expression for item in iterable]

You can add conditions to filter items:


# Only even squares
even_squares = [i * i for i in range(10) if i % 2 == 0]
print(even_squares)  # Output: [0, 4, 16, 36, 64]

# Words longer than 3 characters
words = ["cat", "dog", "elephant", "fox", "giraffe"]
long_words = [word for word in words if len(word) > 3]
print(long_words)  # Output: ['elephant', 'giraffe']

You can apply transformations:


# Convert to uppercase
words = ["hello", "world", "python"]
uppercase = [word.upper() for word in words]
print(uppercase)  # Output: ['HELLO', 'WORLD', 'PYTHON']

# Extract first character
first_chars = [word[0] for word in words]
print(first_chars)  # Output: ['h', 'w', 'p']

You can nest list comprehensions for two-dimensional data:


# Create a multiplication table
table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
for row in table:
    print(row)

Dictionary and set comprehensions work similarly:


# Dictionary comprehension
squares_dict = {i: i * i for i in range(5)}
print(squares_dict)  # Output: {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Set comprehension
even_set = {i for i in range(10) if i % 2 == 0}
print(even_set)  # Output: {0, 2, 4, 6, 8}


CHAPTER TWENTY-NINE: GENERATORS


Generators are functions that return an iterator and generate values on-the-fly rather than storing them all in memory. They use the yield keyword instead of return.


# Generator function
def count_up_to(n):
    count = 1
    while count <= n:
        yield count
        count = count + 1

# Using the generator
counter = count_up_to(5)
for number in counter:
    print(number)


Each time yield is encountered, the function returns a value and pauses. When the generator is called again, it resumes from where it left off.

Generators are memory-efficient because they generate values one at a time:


# This would use a lot of memory with a list
def large_range(n):
    for i in range(n):
        yield i * i

# Only one value is in memory at a time
for value in large_range(1000000):
    if value > 100:
        break
    print(value)


Generator expressions are similar to list comprehensions but use parentheses:


# List comprehension (creates entire list in memory)
squares_list = [i * i for i in range(1000000)]

# Generator expression (generates values on demand)
squares_gen = (i * i for i in range(1000000))

# Use the generator
for square in squares_gen:
    if square > 100:
        break
    print(square)


CHAPTER THIRTY: DECORATORS


Decorators are functions that modify the behavior of other functions. They are a powerful feature for adding functionality without changing the original function.


# Simple decorator
def my_decorator(func):
    def wrapper():
        print("Something before the function")
        func()
        print("Something after the function")
    return wrapper

# Using the decorator with @ syntax
@my_decorator
def say_hello():
    print("Hello!")

# Call the decorated function
say_hello()

The @ syntax is equivalent to: say_hello = my_decorator(say_hello)


Decorators can accept arguments:


# Decorator for functions with arguments
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

result = add(5, 3)
print("Result:", result)


The *args and **kwargs syntax allows the wrapper to accept any number of positional and keyword arguments.

Here is a practical example of a timing decorator:

import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print("Function", func.__name__, "took", end_time - start_time, "seconds")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(2)
    print("Function completed")

slow_function()

Decorators can also take parameters:


def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print("Hello,", name)

greet("Alice")  # Prints greeting 3 times


CHAPTER THIRTY-ONE: CONTEXT MANAGERS


Context managers allow you to allocate and release resources precisely. The with statement is used with context managers. We have already seen this with file handling.

You can create your own context managers using classes:


# Custom context manager
class FileManager:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        print("Opening file")
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_value, traceback):
        print("Closing file")
        if self.file:
            self.file.close()

# Using the context manager
with FileManager("test.txt", "w") as f:
    f.write("Hello, World!")

The __enter__ method is called when entering the with block, and __exit__ is called when leaving it, even if an exception occurs.


You can also create context managers using the contextlib module:


from contextlib import contextmanager

@contextmanager
def file_manager(filename, mode):
    print("Opening file")
    file = open(filename, mode)
    try:
        yield file
    finally:
        print("Closing file")
        file.close()

# Using the context manager
with file_manager("test.txt", "w") as f:
    f.write("Hello, World!")


CHAPTER THIRTY-TWO: REGULAR EXPRESSIONS


Regular expressions are patterns used to match text. Python's re module provides regular expression functionality.


import re

# Simple pattern matching
text = "The quick brown fox jumps over the lazy dog"

# Search for a pattern
match = re.search("fox", text)
if match:
    print("Found 'fox' at position", match.start())

# Find all occurrences
text = "The rain in Spain falls mainly in the plain"
matches = re.findall("ain", text)
print("Found", len(matches), "occurrences of 'ain'")


Regular expressions support special characters for pattern matching. The dot matches any character except newline. The asterisk means zero or more repetitions. The plus means one or more repetitions. The question mark means zero or one repetition.

# Pattern with special characters
pattern = r"\d+"  # One or more digits
text = "I have 3 apples and 12 oranges"
numbers = re.findall(pattern, text)
print("Numbers found:", numbers)  # Output: ['3', '12']

# Email pattern
email_pattern = r"\w+@\w+\.\w+"
text = "Contact us at info@example.com or support@test.org"
emails = re.findall(email_pattern, text)
print("Emails:", emails)

The r prefix before the string creates a raw string, which treats backslashes literally. This is important for regular expressions because they use many backslashes.


You can use groups to extract parts of matches:


# Using groups
pattern = r"(\w+)@(\w+)\.(\w+)"
text = "Email: john@example.com"
match = re.search(pattern, text)
if match:
    print("Username:", match.group(1))
    print("Domain:", match.group(2))
    print("Extension:", match.group(3))


You can replace text using regular expressions:


# Replacing text
text = "The color of the car is red"
new_text = re.sub(r"red", "blue", text)
print(new_text)  # Output: The color of the car is blue

# Replace all digits with X
text = "My phone is 555-1234"
new_text = re.sub(r"\d", "X", text)
print(new_text)  # Output: My phone is XXX-XXXX

CHAPTER THIRTY-THREE: WORKING WITH DATES AND TIMES


The datetime module provides classes for working with dates and times.


from datetime import datetime, date, time, timedelta

# Current date and time
now = datetime.now()
print("Current datetime:", now)

# Current date only
today = date.today()
print("Today's date:", today)

# Creating specific dates
birthday = date(1990, 5, 15)
print("Birthday:", birthday)

# Creating specific times
meeting_time = time(14, 30, 0)  # 2:30 PM
print("Meeting time:", meeting_time)

You can format dates and times as strings:


# Formatting dates
now = datetime.now()
formatted = now.strftime("%Y-%m-%d %H:%M:%S")
print("Formatted:", formatted)

# Different format
formatted = now.strftime("%B %d, %Y")
print("Formatted:", formatted)

You can parse strings into datetime objects:


# Parsing strings
date_string = "2025-10-16"
parsed_date = datetime.strptime(date_string, "%Y-%m-%d")
print("Parsed date:", parsed_date)


The timedelta class represents a duration:


# Working with time differences
today = date.today()
one_week = timedelta(days=7)
next_week = today + one_week
print("One week from now:", next_week)

# Calculate age
birthday = date(1990, 5, 15)
today = date.today()
age = today - birthday
print("Age in days:", age.days)
print("Age in years:", age.days // 365)


CHAPTER THIRTY-FOUR: MULTITHREADING AND CONCURRENCY


Python supports concurrent execution through threading and multiprocessing. Threading allows multiple tasks to run seemingly simultaneously within a single process.


import threading
import time

# Function to run in a thread
def print_numbers():
    for i in range(5):
        print("Number:", i)
        time.sleep(1)

def print_letters():
    for letter in ['A', 'B', 'C', 'D', 'E']:
        print("Letter:", letter)
        time.sleep(1)

# Create threads
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)

# Start threads
thread1.start()
thread2.start()

# Wait for threads to complete
thread1.join()
thread2.join()

print("Both threads finished")

Threads share memory, which can lead to race conditions. You can use locks to synchronize access:


import threading

# Shared resource
counter = 0
lock = threading.Lock()

def increment():
    global counter
    for i in range(100000):
        with lock:  # Acquire lock
            counter = counter + 1
        # Lock is automatically released

# Create multiple threads
threads = []
for i in range(10):
    thread = threading.Thread(target=increment)
    threads.append(thread)
    thread.start()

# Wait for all threads
for thread in threads:
    thread.join()

print("Final counter:", counter)


For CPU-intensive tasks, use multiprocessing instead of threading because of Python's Global Interpreter Lock:


from multiprocessing import Process

def worker(name):
    print("Worker", name, "starting")
    # Do some work
    print("Worker", name, "finished")

# Create processes
processes = []
for i in range(5):
    process = Process(target=worker, args=(i,))
    processes.append(process)
    process.start()

# Wait for all processes
for process in processes:
    process.join()


CHAPTER THIRTY-FIVE: WORKING WITH JSON


JSON (JavaScript Object Notation) is a popular data format for storing and exchanging data. Python's json module makes it easy to work with JSON.


import json

# Python dictionary
person = {
    "name": "Alice",
    "age": 30,
    "city": "New York",
    "hobbies": ["reading", "hiking", "photography"]
}

# Convert to JSON string
json_string = json.dumps(person, indent=2)
print("JSON string:")
print(json_string)

# Convert back to Python object
parsed_person = json.loads(json_string)
print("\nParsed person:")
print(parsed_person["name"])


You can read and write JSON files:


import json

# Write to JSON file
data = {
    "students": [
        {"name": "Alice", "grade": 85},
        {"name": "Bob", "grade": 92},
        {"name": "Charlie", "grade": 78}
    ]
}

with open("students.json", "w") as file:
    json.dump(data, file, indent=2)

# Read from JSON file
with open("students.json", "r") as file:
    loaded_data = json.load(file)
    for student in loaded_data["students"]:
        print(student["name"], ":", student["grade"])


CHAPTER THIRTY-SIX: VIRTUAL ENVIRONMENTS AND PACKAGE MANAGEMENT


As you work on different Python projects, you will need different packages and versions. Virtual environments allow you to create isolated Python environments for each project.

To create a virtual environment, use the venv module that comes with Python:


python -m venv myenv

This creates a directory called "myenv" containing a complete Python installation. To activate it, use:


On Windows: myenv\Scripts\activate

On Mac or Linux: source myenv/bin/activate


Once activated, any packages you install will only be available in this environment. To install packages, use pip:


pip install requests
pip install numpy
pip install pandas

You can list installed packages:


pip list

You can save your project's dependencies to a file:


pip freeze > requirements.txt


Others can install the same dependencies using:


pip install -r requirements.txt

To deactivate the virtual environment:


deactivate

Using virtual environments is considered best practice because it prevents conflicts between different projects' dependencies.


CHAPTER THIRTY-SEVEN: DEBUGGING TECHNIQUES


Debugging is the process of finding and fixing errors in your code. Python provides several tools and techniques for debugging.

The simplest debugging technique is using print statements:


def calculate_average(numbers):
    print("Input numbers:", numbers)  # Debug print
    total = sum(numbers)
    print("Total:", total)  # Debug print
    count = len(numbers)
    print("Count:", count)  # Debug print
    average = total / count
    return average

The built-in debugger pdb allows you to step through code:


import pdb

def problematic_function(x, y):
    pdb.set_trace()  # Execution will pause here
    result = x / y
    return result

problematic_function(10, 0)


When execution reaches set_trace(), you enter an interactive debugging session where you can inspect variables, step through code, and more.

The assert statement checks if a condition is true and raises an error if it is not:


def calculate_discount(price, discount_percent):
    assert 0 <= discount_percent <= 100, "Discount must be between 0 and 100"
    assert price > 0, "Price must be positive"
    
    discount_amount = price * discount_percent / 100
    final_price = price - discount_amount
    return final_price

# This will raise an AssertionError
# result = calculate_discount(100, 150)

Logging is better than print statements for production code:


import logging

# Configure logging
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')

def divide(a, b):
    logging.debug("Dividing %s by %s", a, b)
    try:
        result = a / b
        logging.info("Result: %s", result)
        return result
    except ZeroDivisionError:
        logging.error("Division by zero!")
        return None

divide(10, 2)
divide(10, 0)


CONCLUSION: YOUR PYTHON JOURNEY BEGINS


Congratulations on completing this comprehensive Python tutorial! You have learned an enormous amount, from the very basics of variables and data types to advanced topics like decorators, generators, and multithreading.

Let us review what you have learned. You started by understanding Python's philosophy and installing it on your computer. You learned about variables and the fundamental data types: integers, floats, strings, and booleans. You discovered how to perform operations and get input from users.

You then learned about control flow with if statements, while loops, and for loops. These constructs allow your programs to make decisions and repeat actions. You explored Python's powerful data structures: lists for ordered collections, tuples for immutable sequences, dictionaries for key-value pairs, and sets for unique items.

Functions allowed you to organize your code into reusable blocks. You learned about parameters, return values, scope, and lambda functions. Modules and imports showed you how to use existing code and organize your own code across multiple files.

File handling taught you how to read from and write to files, including working with CSV files. Object-oriented programming introduced you to classes, objects, inheritance, and polymorphism, allowing you to model real-world entities in your code.

Error handling with try-except blocks made your programs more robust. Advanced topics like list comprehensions, generators, and decorators showed you Python's elegant and powerful features. You learned about regular expressions for pattern matching, working with dates and times, and concurrent execution with threading.

Finally, you discovered how to work with JSON data, manage project dependencies with virtual environments, and debug your code effectively.

Where do you go from here? The best way to solidify your Python knowledge is through practice. Start building your own projects. Create a to-do list application. Build a simple game. Write scripts to automate tasks you do regularly. Contribute to open-source projects on GitHub.

Explore Python's rich ecosystem of libraries. For web development, learn Django or Flask. For data science, study NumPy, Pandas, and Matplotlib. For machine learning, investigate scikit-learn, TensorFlow, or PyTorch. For automation, explore Selenium and Beautiful Soup.

Read other people's code. This is one of the best ways to learn. Look at popular Python projects on GitHub and try to understand how they work. Join Python communities online. The Python subreddit, Stack Overflow, and various Discord servers are great places to ask questions and learn from others.

Keep the official Python documentation bookmarked. It is comprehensive and well-written. When you encounter something you do not understand, look it up. Reading documentation is a crucial skill for any programmer.

Remember that becoming proficient at programming takes time and practice. Do not get discouraged when you encounter difficult problems or bugs. Every programmer, no matter how experienced, faces challenges. The key is persistence and a willingness to learn from mistakes.

Python is a wonderful language that opens doors to countless opportunities. Whether you want to build websites, analyze data, create artificial intelligence, automate tasks, or just have fun creating programs, Python gives you the tools to do it. Your journey is just beginning, and the possibilities are endless.

Happy coding, and welcome to the Python community!


No comments: