🐍 Complete Python Tutorial: Beginner to Advanced

Master Python programming from scratch with practical examples and real-world applications

What Is Python and How It Works

Python is a programming language that reads almost like English, making it the perfect first language for anyone learning to code. Created by Guido van Rossum in 1991, Python was designed with one core philosophy: simplicity. While other languages might require you to memorize complex symbols and cryptic syntax, Python lets you focus on solving problems rather than fighting with the language itself.

Think of programming languages as different ways to give instructions to a computer. Some languages are like speaking in formal legal jargon precise but difficult. Python is like having a conversation with a smart assistant who understands what you mean even when you use everyday language.

Why Learn Python in 2026?

Python has become the world's most popular programming language, and for good reasons that matter to you as a learner:

How Python Actually Works

When you write Python code, you're creating a text file with instructions. But computers don't understand English like commands they only understand machine code (ones and zeros). Python acts as a translator between your readable code and the computer's language.

Here's what happens when you run a Python program:

  1. You write code in a .py file using plain text
  2. Python interpreter reads your code line by line
  3. The interpreter translates each instruction into machine code
  4. Your computer executes the machine code
  5. Results appear on your screen

This interpretation happens instantly you won't notice any delay. The key insight is that Python is an interpreted language, meaning it translates and runs your code on the fly, rather than requiring a separate "compilation" step like languages such as C++ or Java.

Python 2 vs Python 3: Which Should You Learn?

You might see references to Python 2 and Python 3. Here's what you need to know: Learn Python 3. Period.

Python 2 was officially discontinued on January 1, 2020. While some legacy systems still use it, all new development happens in Python 3. This tutorial teaches Python 3, which is the only version that matters for anyone starting today.

Key Difference: The most visible difference is that Python 2 uses print "Hello" while Python 3 uses print("Hello"). If you see code without parentheses in print statements, it's outdated Python 2 code.

What Can You Build with Python?

Let's ground this in reality. Here's what Python actually powers:

Installing Python and Setting Up the Environment

Before you can write Python code, you need to install Python on your computer. Don't worry this process is straightforward and takes about 5 minutes.

Installing Python on Windows

  1. Go to python.org (the official Python website)
  2. Click the Downloads tab
  3. Click the big yellow button that says Download Python 3.x.x (the numbers will be the latest version)
  4. Run the downloaded installer
  5. CRITICAL: Check the box that says "Add Python to PATH" at the bottom of the installer
  6. Click "Install Now"
  7. Wait for installation to complete
Common Mistake: Forgetting to check "Add Python to PATH" is the #1 beginner error. Without this, Windows won't recognize Python commands. If you forgot, uninstall and reinstall Python with this option checked.

Installing Python on Mac

Macs come with Python 2.7 pre-installed, but remember we want Python 3:

  1. Go to python.org/downloads
  2. Download the latest Python 3 installer for macOS
  3. Open the downloaded .pkg file
  4. Follow the installation wizard
  5. When complete, Python 3 will be installed alongside the old Python 2

Installing Python on Linux

Most Linux distributions include Python 3. Check by opening a terminal and typing:

python3 --version

If Python 3 isn't installed, use your package manager:

# Ubuntu/Debian
    sudo apt update
    sudo apt install python3
    
    # Fedora
    sudo dnf install python3
    
    # Arch Linux
    sudo pacman -S python

Verifying Your Installation

After installation, verify Python works:

  1. Open your terminal/command prompt
  2. Type python --version (or python3 --version on Mac/Linux)
  3. You should see something like Python 3.11.4

If you see a version number starting with 3, congratulations Python is installed!

Choosing a Code Editor

You can write Python in any text editor, but using an editor designed for code makes life much easier. Here are the best options for beginners:

Visual Studio Code (Recommended for Beginners)

PyCharm Community Edition

IDLE (Comes with Python)

My Recommendation: Start with VS Code. It's beginner friendly but powerful enough that you won't outgrow it. Plus, if you learn other languages later, VS Code supports them all.

Your First Python Program

Let's write your first program to confirm everything works:

  1. Open your chosen editor
  2. Create a new file called hello.py
  3. Type this exact code:
print("Hello, Python!")
  1. Save the file
  2. Open your terminal/command prompt
  3. Navigate to where you saved the file
  4. Run: python hello.py
Output: Hello, Python!

If you see "Hello, Python!" appear, you've successfully written and executed your first Python program. Welcome to programming!

Python Syntax and Indentation

Python's syntax the rules for writing code is famous for being clean and readable. However, Python has one unique characteristic that trips up many beginners: indentation matters.

In most programming languages, curly braces {} define code blocks. Python uses indentation (spaces or tabs) instead. This forces you to write visually organized code, which makes Python programs easier to read than most other languages.

Understanding Indentation

In Python, indentation isn't just for looks—it defines the structure of your code. Consider this example:

if True:
        print("This is indented")
        print("So is this")
    print("This is not indented")

The first two print statements are indented, so they belong to the if block. The third print isn't indented, so it's outside the if block and runs regardless of the condition.

How Much Indentation?

The Python community standard is 4 spaces per indentation level. Your editor should automatically convert Tab key presses to 4 spaces.

# Good - 4 spaces
    if True:
        print("Correct indentation")
    
    # Bad - inconsistent spacing
    if True:
      print("2 spaces")
          print("6 spaces - ERROR!")
Common Error: IndentationError
If you mix tabs and spaces, or use inconsistent spacing, Python will throw an IndentationError. Pick one method (4 spaces recommended) and stick with it throughout your entire file.

Statements and Comments

A statement is a single instruction in Python. Most statements fit on one line:

name = "Alice"
    age = 30
    print(name)

Comments are notes you write for yourself (or other programmers). Python ignores everything after a # symbol:

# This is a comment - Python ignores this
    name = "Alice"  # You can also comment after code
    
    # Use comments to explain WHY, not WHAT
    # Bad comment:
    x = x + 1  # Add 1 to x (obvious from code)
    
    # Good comment:
    x = x + 1  # Account for zero-indexed array

Multi-Line Statements

Sometimes a statement is too long for one line. You can split it using backslash \ or implied line continuation inside parentheses:

# Using backslash
    total = 1 + 2 + 3 + \
            4 + 5 + 6
    
    # Better: implied continuation (no backslash needed)
    total = (1 + 2 + 3 +
             4 + 5 + 6)
    
    # Also works for function calls
    print("This is a very long string that "
          "spans multiple lines")

Python Naming Conventions

While Python doesn't enforce naming styles, following conventions makes your code more readable:

# Variables and functions: lowercase with underscores
    user_name = "Alice"
    def calculate_total():
        pass
    
    # Constants: UPPERCASE with underscores
    MAX_SIZE = 100
    PI = 3.14159
    
    # Classes: CapitalizedWords (we'll cover classes later)
    class UserAccount:
        pass

Python Variables and Data Types

Variables are containers that store data. Think of them as labeled boxes where you put information that you want to use later in your program.

Creating Variables

In Python, creating a variable is incredibly simple—just assign a value to a name:

message = "Hello, World!"
    age = 25
    price = 19.99
    is_valid = True

Notice you don't need to declare the type (like "this is a number" or "this is text"). Python figures it out automatically from the value you assign. This is called dynamic typing.

Variable Naming Rules

Variables can be named almost anything, but must follow these rules:

# Valid variable names
    name = "Alice"
    age_2 = 30
    _private = "hidden"
    firstName = "John"
    
    # Invalid variable names
    2name = "Alice"      # Can't start with number
    my-name = "Alice"    # Hyphens not allowed
    for = "loop"         # 'for' is a keyword

Understanding Data Types

Python has several built in data types. Here are the fundamental ones:

# Integer (whole numbers)
    age = 25
    year = 2024
    negative = -10
    
    # Float (decimal numbers)
    price = 19.99
    temperature = -5.5
    pi = 3.14159
    
    # String (text)
    name = "Alice"
    message = 'Hello, World!'
    multiline = """This is
    a multi-line
    string"""
    
    # Boolean (True or False)
    is_active = True
    has_permission = False
    
    # NoneType (represents "nothing" or "no value")
    result = None

Checking Types

Use the type() function to check what type a variable is:

age = 25
    print(type(age))  # 
    
    price = 19.99
    print(type(price))  # 
    
    name = "Alice"
    print(type(name))  # 
    
    is_valid = True
    print(type(is_valid))  # 

Type Conversion

You can convert between types explicitly:

# String to integer
    age_str = "25"
    age_int = int(age_str)
    print(age_int + 5)  # 30
    
    # Integer to string
    number = 42
    text = str(number)
    print("The answer is " + text)
    
    # String to float
    price_str = "19.99"
    price_float = float(price_str)
    
    # Be careful - this causes an error:
    bad_conversion = int("Hello")  # ValueError!
Common Mistake: Trying to convert non-numeric strings to numbers causes a ValueError. Always validate data before converting, especially when dealing with user input.

Numbers, Strings, and Booleans

Let's dive deeper into Python's three most commonly used data types.

Working with Numbers

Python handles both integers (whole numbers) and floats (decimals):

# Basic arithmetic
    addition = 10 + 5        # 15
    subtraction = 10 - 5     # 5
    multiplication = 10 * 5  # 50
    division = 10 / 3        # 3.3333...
    
    # Integer division (rounds down)
    floor_division = 10 // 3  # 3
    
    # Modulo (remainder)
    remainder = 10 % 3       # 1
    
    # Exponentiation
    power = 2 ** 3           # 8 (2 cubed)
    
    # Order of operations (PEMDAS applies)
    result = 2 + 3 * 4       # 14 (not 20!)
    result = (2 + 3) * 4     # 20 (parentheses first)

Python has no limit on integer size you can work with numbers as large as your computer's memory allows:

huge_number = 123456789012345678901234567890
    print(huge_number * 2)  # Works fine!

String Manipulation

Strings are sequences of characters. Python provides powerful string operations:

# Creating strings
    single_quotes = 'Hello'
    double_quotes = "World"
    both_work = 'Both "quotes" work!'
    
    # String concatenation
    first_name = "Alice"
    last_name = "Smith"
    full_name = first_name + " " + last_name  # "Alice Smith"
    
    # String repetition
    laugh = "Ha" * 3  # "HaHaHa"
    
    # String length
    message = "Hello"
    length = len(message)  # 5
    
    # Accessing characters (zero-indexed!)
    first_char = message[0]   # "H"
    last_char = message[-1]   # "o"
    
    # String slicing
    substring = message[1:4]  # "ell" (from index 1 to 4, exclusive)

String Methods

Strings have built-in methods for common operations:

text = "Hello, World!"
    
    # Case changes
    print(text.upper())      # "HELLO, WORLD!"
    print(text.lower())      # "hello, world!"
    print(text.title())      # "Hello, World!"
    
    # Searching
    print(text.find("World"))     # 7 (index where "World" starts)
    print(text.count("l"))        # 3 (number of "l"s)
    print("World" in text)        # True
    
    # Replacing
    new_text = text.replace("World", "Python")  # "Hello, Python!"
    
    # Trimming whitespace
    messy = "  spaces  "
    clean = messy.strip()    # "spaces"
    
    # Splitting into list
    words = text.split(", ") # ["Hello", "World!"]

F-Strings: Modern String Formatting

F-strings (formatted string literals) are the best way to insert variables into strings:

name = "Alice"
    age = 30
    city = "New York"
    
    # Old way (avoid)
    message = "My name is " + name + " and I am " + str(age) + " years old."
    
    # F-string way (recommended)
    message = f"My name is {name} and I am {age} years old."
    
    # Can include expressions
    message = f"Next year I'll be {age + 1}."
    
    # Formatting numbers
    price = 19.99
    print(f"Price: ${price:.2f}")  # "Price: $19.99" (2 decimal places)

Boolean Logic

Booleans represent True or False. They're essential for decision-making in code:

# Creating booleans
    is_logged_in = True
    has_permission = False
    
    # Comparison operations return booleans
    print(5 > 3)        # True
    print(10 == 10)     # True
    print("a" == "A")   # False (case-sensitive)
    
    # Logical operators
    age = 25
    has_license = True
    
    can_drive = age >= 18 and has_license  # Both must be True
    can_enter = age >= 18 or has_license   # At least one must be True
    is_minor = not (age >= 18)              # Inverts the boolean
    
    # Truthiness: non-boolean values in boolean context
    if "Hello":        # Non-empty strings are truthy
        print("This runs")
    
    if 0:              # Zero is falsy
        print("This doesn't run")
Falsy Values in Python: These evaluate to False in boolean context: False, None, 0, 0.0, '' (empty string), [] (empty list), {} (empty dict). Everything else is truthy.

Python Operators

Operators are symbols that perform operations on values. We've seen some already, but let's cover them systematically.

Arithmetic Operators

# Basic math
    x = 10
    y = 3
    
    print(x + y)   # Addition: 13
    print(x - y)   # Subtraction: 7
    print(x * y)   # Multiplication: 30
    print(x / y)   # Division: 3.333...
    print(x // y)  # Floor division: 3
    print(x % y)   # Modulo (remainder): 1
    print(x ** y)  # Exponentiation: 1000 (10³)
    
    # Compound assignment
    count = 0
    count += 1    # Same as: count = count + 1
    count -= 1    # Same as: count = count - 1
    count *= 2    # Same as: count = count * 2
    count /= 2    # Same as: count = count / 2

Comparison Operators

a = 10
    b = 5
    
    print(a == b)   # Equal: False
    print(a != b)   # Not equal: True
    print(a > b)    # Greater than: True
    print(a < b)    # Less than: False
    print(a >= b)   # Greater or equal: True
    print(a <= b)   # Less or equal: False
    
    # String comparisons
    print("apple" < "banana")  # True (alphabetical order)
    print("10" == 10)          # False (different types!)

Logical Operators

# and - both conditions must be True
    age = 25
    income = 50000
    approved = age >= 18 and income >= 30000  # True
    
    # or - at least one condition must be True
    is_weekend = True
    is_holiday = False
    can_sleep_in = is_weekend or is_holiday  # True
    
    # not - inverts the boolean
    is_raining = False
    is_sunny = not is_raining  # True

Identity and Membership Operators

# is - checks if two variables point to the same object
    a = [1, 2, 3]
    b = a
    c = [1, 2, 3]
    
    print(a is b)      # True (same object)
    print(a is c)      # False (different objects, same values)
    print(a == c)      # True (same values)
    
    # in - checks if value exists in sequence
    fruits = ["apple", "banana", "cherry"]
    print("apple" in fruits)       # True
    print("grape" in fruits)       # False
    print("a" in "apple")          # True (works with strings too!)

Input and Output

Programs need to interact with users. Input lets users provide data, while output displays results.

The print() Function

We've used print() extensively already. Here are its advanced features:

# Basic printing
    print("Hello, World!")
    
    # Multiple arguments
    print("Hello", "World")  # Outputs: Hello World (space added automatically)
    
    # Custom separator
    print("Hello", "World", sep="-")  # Outputs: Hello-World
    
    # Custom ending (default is newline)
    print("Hello", end=" ")
    print("World")  # Outputs: Hello World (on same line)
    
    # Printing variables
    name = "Alice"
    age = 30
    print("Name:", name, "Age:", age)
    
    # F-string (best for mixing text and variables)
    print(f"Name: {name}, Age: {age}")

The input() Function

The input() function gets text from the user. It always returns a string:

# Basic input
    name = input("What is your name? ")
    print(f"Hello, {name}!")
    
    # Converting input to numbers
    age_str = input("What is your age? ")
    age = int(age_str)  # Convert string to integer
    
    # One-liner conversion
    age = int(input("What is your age? "))
    
    # Always validate user input!
    try:
        age = int(input("Enter your age: "))
        print(f"You are {age} years old")
    except ValueError:
        print("That's not a valid number!")
Critical: input() always returns a string, even if the user types a number. If you need a number, you must convert it with int() or float().

Practical Input/Output Example

# Simple calculator
    print("=== Simple Calculator ===")
    
    num1 = float(input("Enter first number: "))
    operator = input("Enter operator (+, -, *, /): ")
    num2 = float(input("Enter second number: "))
    
    if operator == "+":
        result = num1 + num2
    elif operator == "-":
        result = num1 - num2
    elif operator == "*":
        result = num1 * num2
    elif operator == "/":
        if num2 != 0:
            result = num1 / num2
        else:
            result = "Error: Division by zero"
    else:
        result = "Error: Invalid operator"
    
    print(f"Result: {result}")

Python Conditional Statements (if, elif, else)

Conditional statements let your program make decisions based on conditions. They're like flowcharts in code form.

The if Statement

The simplest conditional executes code only if a condition is true:

age = 20
    
    if age >= 18:
        print("You are an adult")
        print("You can vote")
    
    print("This runs regardless")

Notice the indentation everything indented after the if statement only runs if the condition is true.

if-else: Two Paths

temperature = 15
    
    if temperature > 20:
        print("It's warm outside")
        print("Wear light clothes")
    else:
        print("It's cold outside")
        print("Wear a jacket")
    
    print("Have a nice day!")

elif: Multiple Conditions

elif (short for "else if") checks additional conditions:

score = 85
    
    if score >= 90:
        grade = "A"
    elif score >= 80:
        grade = "B"
    elif score >= 70:
        grade = "C"
    elif score >= 60:
        grade = "D"
    else:
        grade = "F"
    
    print(f"Your grade is: {grade}")
How elif Works: Python checks conditions in order. As soon as one is true, it executes that block and skips the rest. If score is 85, it matches the second condition (>= 80) and never checks the remaining ones.

Nested Conditionals

You can put conditionals inside other conditionals:

age = 25
    has_license = True
    
    if age >= 18:
        if has_license:
            print("You can drive")
        else:
            print("You need a license to drive")
    else:
        print("You're too young to drive")
    
    # Better: combine conditions with 'and'
    if age >= 18 and has_license:
        print("You can drive")
    elif age >= 18:
        print("You need a license to drive")
    else:
        print("You're too young to drive")

Ternary Operator: One-Line Conditionals

For simple if-else statements, Python offers a compact syntax:

# Regular if-else
    age = 20
    if age >= 18:
        status = "adult"
    else:
        status = "minor"
    
    # Ternary operator (one line)
    status = "adult" if age >= 18 else "minor"
    
    # Useful for simple assignments
    max_value = a if a > b else b
    message = "Pass" if score >= 60 else "Fail"

Python Loops (for, while)

Loops let you repeat code multiple times without writing it repeatedly. They're essential for processing collections of data and automating repetitive tasks.

The for Loop: Iterating Over Sequences

The for loop is perfect when you know what you want to iterate over:

# Loop over a range of numbers
    for i in range(5):
        print(i)
    # Outputs: 0, 1, 2, 3, 4
    
    # Loop over a list
    fruits = ["apple", "banana", "cherry"]
    for fruit in fruits:
        print(f"I like {fruit}")
    
    # Loop over a string
    for letter in "Python":
        print(letter)
    
    # Range with start and end
    for i in range(1, 6):  # 1 to 5 (6 is exclusive)
        print(i)
    
    # Range with step
    for i in range(0, 10, 2):  # Even numbers: 0, 2, 4, 6, 8
        print(i)

The while Loop: Repeating Until a Condition Changes

The while loop continues until its condition becomes false:

# Count to 5
    count = 1
    while count <= 5:
        print(count)
        count += 1
    
    # User input loop
    password = ""
    while password != "secret":
        password = input("Enter password: ")
    print("Access granted!")
    
    # Be careful of infinite loops!
    # while True:  # Runs forever unless you break out
    #     print("Forever...")
Infinite Loop Danger: Make sure your while loop condition eventually becomes false! If it doesn't, your program will run forever. Always ensure the loop variable changes inside the loop.

Loop Control: break and continue

# break - exit the loop immediately
    for i in range(10):
        if i == 5:
            break  # Stop when i reaches 5
        print(i)
    # Outputs: 0, 1, 2, 3, 4
    
    # continue - skip to next iteration
    for i in range(5):
        if i == 2:
            continue  # Skip 2
        print(i)
    # Outputs: 0, 1, 3, 4
    
    # Real-world example: finding first matching item
    numbers = [1, 3, 7, 2, 9, 4]
    for num in numbers:
        if num % 2 == 0:  # First even number
            print(f"Found even number: {num}")
            break

Nested Loops

Loops can contain other loops:

# Multiplication table
    for i in range(1, 6):
        for j in range(1, 6):
            print(f"{i} x {j} = {i*j}")
        print("---")  # Separator between rows
    
    # Pattern printing
    for i in range(5):
        for j in range(i + 1):
            print("*", end="")
        print()  # New line
    # Outputs:
    # *
    # **
    # ***
    # ****
    # *****

The else Clause in Loops

Python loops can have an else clause that runs if the loop completes normally (not broken by break):

# Search for a number
    numbers = [1, 3, 5, 7, 9]
    search = 6
    
    for num in numbers:
        if num == search:
            print(f"Found {search}!")
            break
    else:
        print(f"{search} not found")  # Runs because we didn't break

Python Functions Explained

Functions are reusable blocks of code that perform specific tasks. Think of them as mini-programs within your program.

Defining Functions

# Basic function
    def greet():
        print("Hello!")
    
    # Call the function
    greet()  # Outputs: Hello!
    
    # Function with parameters
    def greet_person(name):
        print(f"Hello, {name}!")
    
    greet_person("Alice")  # Outputs: Hello, Alice!
    
    # Multiple parameters
    def add_numbers(a, b):
        result = a + b
        print(f"{a} + {b} = {result}")
    
    add_numbers(5, 3)  # Outputs: 5 + 3 = 8

Return Values

Functions can send results back using return:

def add(a, b):
        return a + b
    
    result = add(5, 3)
    print(result)  # 8
    
    # Can return multiple values
    def get_user_info():
        name = "Alice"
        age = 30
        city = "NYC"
        return name, age, city
    
    name, age, city = get_user_info()
    print(f"{name}, {age}, from {city}")
    
    # Function stops at return
    def check_age(age):
        if age < 18:
            return "Too young"
        return "Old enough"  # Only reached if age >= 18

Default Parameters

def greet(name="Guest"):
        print(f"Hello, {name}!")
    
    greet("Alice")  # Hello, Alice!
    greet()         # Hello, Guest! (uses default)
    
    # Multiple default parameters
    def create_profile(name, age=18, city="Unknown"):
        print(f"Name: {name}, Age: {age}, City: {city}")
    
    create_profile("Bob")                    # Uses defaults for age and city
    create_profile("Alice", 25)              # Uses default for city
    create_profile("Charlie", 30, "NYC")     # No defaults used

Keyword Arguments

def describe_pet(animal_type, pet_name, age):
        print(f"I have a {age}-year-old {animal_type} named {pet_name}")
    
    # Positional arguments
    describe_pet("dog", "Buddy", 3)
    
    # Keyword arguments (can be in any order)
    describe_pet(pet_name="Buddy", age=3, animal_type="dog")
    
    # Mix positional and keyword (positional must come first)
    describe_pet("dog", pet_name="Buddy", age=3)

*args and **kwargs

Accept variable numbers of arguments:

# *args - variable number of positional arguments
    def sum_all(*numbers):
        total = 0
        for num in numbers:
            total += num
        return total
    
    print(sum_all(1, 2, 3))        # 6
    print(sum_all(1, 2, 3, 4, 5))  # 15
    
    # **kwargs - variable number of keyword arguments
    def print_info(**details):
        for key, value in details.items():
            print(f"{key}: {value}")
    
    print_info(name="Alice", age=30, city="NYC")

Lambda Functions: Anonymous Functions

Lambda functions are small, unnamed functions for simple operations:

# Regular function
    def square(x):
        return x ** 2
    
    # Lambda equivalent
    square = lambda x: x ** 2
    
    print(square(5))  # 25
    
    # Lambda with multiple arguments
    add = lambda a, b: a + b
    print(add(3, 5))  # 8
    
    # Common use: with map(), filter(), sorted()
    numbers = [1, 2, 3, 4, 5]
    squared = list(map(lambda x: x**2, numbers))
    print(squared)  # [1, 4, 9, 16, 25]
    
    evens = list(filter(lambda x: x % 2 == 0, numbers))
    print(evens)  # [2, 4]

Python Lists, Tuples, Sets, and Dictionaries

Python provides four main collection types. Each has unique characteristics that make it suitable for different tasks.

Lists: Ordered, Mutable Collections

# Creating lists
    fruits = ["apple", "banana", "cherry"]
    numbers = [1, 2, 3, 4, 5]
    mixed = [1, "hello", True, 3.14]
    
    # Accessing elements
    print(fruits[0])    # "apple"
    print(fruits[-1])   # "cherry" (last item)
    print(fruits[0:2])  # ["apple", "banana"] (slicing)
    
    # Modifying lists
    fruits[0] = "orange"  # Change first item
    fruits.append("grape")  # Add to end
    fruits.insert(1, "kiwi")  # Insert at index 1
    fruits.remove("banana")  # Remove by value
    deleted = fruits.pop()  # Remove and return last item
    del fruits[0]  # Delete by index
    
    # List methods
    numbers = [3, 1, 4, 1, 5]
    numbers.sort()  # Sort in place
    numbers.reverse()  # Reverse in place
    print(numbers.count(1))  # Count occurrences of 1
    print(numbers.index(4))  # Find index of 4
    
    # List comprehension (elegant way to create lists)
    squares = [x**2 for x in range(5)]  # [0, 1, 4, 9, 16]
    evens = [x for x in range(10) if x % 2 == 0]  # [0, 2, 4, 6, 8]

Tuples: Ordered, Immutable Collections

# Creating tuples
    coordinates = (10, 20)
    person = ("Alice", 30, "NYC")
    single_item = (42,)  # Comma needed for single-item tuple
    
    # Accessing (same as lists)
    print(person[0])  # "Alice"
    print(person[-1])  # "NYC"
    
    # Tuples are immutable - can't change
    # person[0] = "Bob"  # ERROR!
    
    # But can unpack
    name, age, city = person
    print(name)  # "Alice"
    
    # Why use tuples?
    # 1. Faster than lists
    # 2. Protect data from modification
    # 3. Can be dictionary keys (lists can't)

Sets: Unordered, Unique Collections

# Creating sets
    fruits = {"apple", "banana", "cherry"}
    numbers = {1, 2, 3, 4, 5}
    
    # Automatically removes duplicates
    numbers = {1, 2, 2, 3, 3, 3}  # Becomes {1, 2, 3}
    
    # Adding and removing
    fruits.add("orange")
    fruits.remove("banana")  # Error if not found
    fruits.discard("grape")  # No error if not found
    
    # Set operations
    a = {1, 2, 3, 4}
    b = {3, 4, 5, 6}
    
    print(a | b)  # Union: {1, 2, 3, 4, 5, 6}
    print(a & b)  # Intersection: {3, 4}
    print(a - b)  # Difference: {1, 2}
    
    # Membership testing (very fast)
    print(3 in a)  # True

Dictionaries: Key-Value Pairs

# Creating dictionaries
    person = {
        "name": "Alice",
        "age": 30,
        "city": "NYC"
    }
    
    # Accessing values
    print(person["name"])  # "Alice"
    print(person.get("age"))  # 30
    print(person.get("country", "USA"))  # "USA" (default if key missing)
    
    # Adding/modifying
    person["email"] = "alice@example.com"  # Add new key
    person["age"] = 31  # Modify existing
    
    # Removing
    del person["city"]
    removed_value = person.pop("email")
    
    # Dictionary methods
    print(person.keys())    # dict_keys(['name', 'age'])
    print(person.values())  # dict_values(['Alice', 31])
    print(person.items())   # dict_items([('name', 'Alice'), ('age', 31)])
    
    # Looping through dictionary
    for key, value in person.items():
        print(f"{key}: {value}")
    
    # Dictionary comprehension
    squares = {x: x**2 for x in range(5)}  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Choosing the Right Collection:
- Use lists when you need ordered, changeable data
- Use tuples when you need ordered, unchangeable data
- Use sets when you need unique values and fast membership testing
- Use dictionaries when you need key-value associations

Common Beginner Mistakes in Python

Every Python beginner makes these mistakes. Learning to recognize and fix them will save you hours of frustration.

Mistake #1: Indentation Errors

# Wrong - inconsistent indentation
    def greet():
      print("Hello")  # 2 spaces
        print("World")  # 4 spaces - ERROR!
    
    # Right - consistent indentation
    def greet():
        print("Hello")  # 4 spaces
        print("World")  # 4 spaces

Mistake #2: Forgetting Colons

# Wrong - missing colons
    if age >= 18
        print("Adult")
    
    # Right
    if age >= 18:
        print("Adult")
    
    # Also needed for loops, functions, classes
    for i in range(5):  # Colon here!
    def my_function():   # And here!
    class MyClass:       # And here!

Mistake #3: Using = Instead of ==

# Wrong - assignment in condition
    if age = 18:  # ERROR! This assigns, not compares
        print("Adult")
    
    # Right - comparison operator
    if age == 18:
        print("Adult")

Mistake #4: Modifying List While Iterating

# Wrong - modifying list during iteration
    numbers = [1, 2, 3, 4, 5]
    for num in numbers:
        if num % 2 == 0:
            numbers.remove(num)  # Causes unexpected behavior!
    
    # Right - iterate over copy
    numbers = [1, 2, 3, 4, 5]
    for num in numbers.copy():
        if num % 2 == 0:
            numbers.remove(num)
    
    # Better - list comprehension
    numbers = [1, 2, 3, 4, 5]
    numbers = [num for num in numbers if num % 2 != 0]

Mistake #5: Not Converting input() to Number

# Wrong
    age = input("Enter age: ")
    if age >= 18:  # ERROR! Comparing string to number
        print("Adult")
    
    # Right
    age = int(input("Enter age: "))
    if age >= 18:
        print("Adult")

Mistake #6: Mutable Default Arguments

# Wrong - dangerous!
    def add_item(item, items=[]):  # Default list is created once!
        items.append(item)
        return items
    
    print(add_item("apple"))   # ['apple']
    print(add_item("banana"))  # ['apple', 'banana'] - Unexpected!
    
    # Right
    def add_item(item, items=None):
        if items is None:
            items = []
        items.append(item)
        return items
Pro Tip: When you get an error, read the error message carefully! Python tells you exactly what went wrong and which line caused the error. The last line of the error message is usually the most important.

🎓 Congratulations! You've completed the Beginner section of this Python tutorial. You now understand variables, data types, control flow, functions, and collections—the fundamental building blocks of Python programming.

Next Steps: The Intermediate section awaits, where you'll learn about modules, file handling, object-oriented programming, and working with external data. Take a break, practice what you've learned by building small projects, then continue when you're ready.

Python Modules and Packages

As your programs grow, organizing code becomes essential. Modules are Python files containing functions, classes, and variables that you can import and reuse. Packages are collections of related modules organized in directories.

Creating Your First Module

A module is simply a Python file. Create a file called math_utils.py:

# math_utils.py
    def add(a, b):
        return a + b
    
    def subtract(a, b):
        return a - b
    
    def multiply(a, b):
        return a * b
    
    PI = 3.14159

Now you can import and use this module in another file:

# main.py
    import math_utils
    
    result = math_utils.add(5, 3)
    print(result)  # 8
    
    print(math_utils.PI)  # 3.14159

Import Variations

# Import entire module
    import math_utils
    result = math_utils.add(5, 3)
    
    # Import specific items
    from math_utils import add, PI
    result = add(5, 3)  # No need for math_utils prefix
    print(PI)
    
    # Import with alias
    import math_utils as mu
    result = mu.add(5, 3)
    
    # Import everything (avoid this - pollutes namespace)
    from math_utils import *
    result = add(5, 3)  # Works but risky
Best Practice: Avoid from module import * because it imports everything, making it unclear where functions come from. Use explicit imports or import the whole module.

The Standard Library: Built-in Modules

Python includes dozens of modules for common tasks:

# Working with dates
    import datetime
    
    now = datetime.datetime.now()
    print(now)  # 2026-01-25 14:30:45.123456
    
    birthday = datetime.date(1990, 5, 15)
    age_days = (datetime.date.today() - birthday).days
    
    # Random numbers
    import random
    
    number = random.randint(1, 100)  # Random integer 1-100
    choice = random.choice(['rock', 'paper', 'scissors'])
    random.shuffle(my_list)  # Shuffle list in place
    
    # Math operations
    import math
    
    print(math.sqrt(16))  # 4.0
    print(math.ceil(4.2))  # 5
    print(math.floor(4.8))  # 4
    
    # Operating system operations
    import os
    
    print(os.getcwd())  # Current directory
    os.mkdir('new_folder')  # Create directory
    files = os.listdir('.')  # List files in current directory

Creating Packages

Packages organize related modules into directories. Here's a simple package structure:

my_package/
        __init__.py          # Makes this directory a package
        math_operations.py
        string_operations.py

The __init__.py file can be empty or contain initialization code:

# my_package/__init__.py
    print("Package imported!")
    
    # Can import specific items to package level
    from .math_operations import add
    from .string_operations import capitalize

Using your package:

from my_package import add
    from my_package.string_operations import reverse_string
    
    result = add(5, 3)
    text = reverse_string("hello")

Python Scope and Namespaces

Understanding scope where variables are accessible—prevents bugs and helps you write cleaner code. Python uses the LEGB rule to resolve names: Local, Enclosing, Global, Built-in.

Local Scope

Variables created inside functions are local to that function:

def my_function():
        local_var = "I'm local"
        print(local_var)  # Works here
    
    my_function()
    print(local_var)  # ERROR! local_var doesn't exist outside function

Global Scope

Variables created outside functions are global:

global_var = "I'm global"
    
    def my_function():
        print(global_var)  # Can read global variables
    
    my_function()  # Prints: I'm global
    print(global_var)  # Also works here

Modifying Global Variables

counter = 0
    
    def increment():
        global counter  # Declare you're using global variable
        counter += 1
    
    increment()
    print(counter)  # 1
    
    # Without 'global' keyword
    def bad_increment():
        counter = 0  # Creates NEW local variable
        counter += 1
    
    bad_increment()
    print(counter)  # Still 1 (global unchanged)

Enclosing Scope (Closures)

def outer():
        x = "outer"
        
        def inner():
            print(x)  # Can access outer function's variables
        
        inner()
    
    outer()  # Prints: outer
    
    # Modifying enclosing scope
    def counter_factory():
        count = 0
        
        def increment():
            nonlocal count  # Modify enclosing function's variable
            count += 1
            return count
        
        return increment
    
    counter = counter_factory()
    print(counter())  # 1
    print(counter())  # 2
    print(counter())  # 3

The LEGB Rule in Action

x = "global"
    
    def outer():
        x = "enclosing"
        
        def inner():
            x = "local"
            print(x)  # Prints "local" (L wins)
        
        inner()
        print(x)  # Prints "enclosing" (E wins)
    
    outer()
    print(x)  # Prints "global" (G wins)

Python Error Handling (try, except, finally)

Errors are inevitable. Good error handling makes your programs robust and user-friendly instead of crashing with cryptic messages.

Basic Try-Except

# Without error handling
    number = int(input("Enter a number: "))  # Crashes if user enters text!
    
    # With error handling
    try:
        number = int(input("Enter a number: "))
        print(f"You entered: {number}")
    except ValueError:
        print("That's not a valid number!")

Catching Multiple Exceptions

try:
        numerator = int(input("Enter numerator: "))
        denominator = int(input("Enter denominator: "))
        result = numerator / denominator
        print(f"Result: {result}")
    except ValueError:
        print("Please enter valid numbers")
    except ZeroDivisionError:
        print("Cannot divide by zero!")
    except Exception as e:
        print(f"Unexpected error: {e}")

The else and finally Clauses

try:
        file = open("data.txt", "r")
        data = file.read()
    except FileNotFoundError:
        print("File not found!")
    else:
        # Runs only if no exception occurred
        print("File read successfully")
        print(f"Content: {data}")
    finally:
        # Always runs, even if exception occurred
        if 'file' in locals():
            file.close()
        print("Cleanup complete")

Raising Exceptions

def validate_age(age):
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age seems unrealistic")
        return True
    
    try:
        validate_age(-5)
    except ValueError as e:
        print(f"Validation error: {e}")

Custom Exceptions

class InsufficientFundsError(Exception):
        """Raised when account has insufficient funds"""
        pass
    
    class BankAccount:
        def __init__(self, balance):
            self.balance = balance
        
        def withdraw(self, amount):
            if amount > self.balance:
                raise InsufficientFundsError(
                    f"Cannot withdraw ${amount}. Balance: ${self.balance}"
                )
            self.balance -= amount
            return self.balance
    
    account = BankAccount(100)
    try:
        account.withdraw(150)
    except InsufficientFundsError as e:
        print(e)
When to Use Try-Except: Use it for operations that might fail through no fault of your code file operations, network requests, user input, external API calls. Don't use it to hide programming errors you should fix.

Working with Files (Read/Write)

File handling is essential for storing data, processing logs, and working with external information. Python makes file operations straightforward.

Reading Files

# Reading entire file
    with open("data.txt", "r") as file:
        content = file.read()
        print(content)
    
    # Reading line by line
    with open("data.txt", "r") as file:
        for line in file:
            print(line.strip())  # strip() removes newline
    
    # Reading into list
    with open("data.txt", "r") as file:
        lines = file.readlines()
        print(lines)  # List of lines with newlines
The 'with' Statement: Using with automatically closes the file when done, even if an error occurs. Always prefer this over manually opening and closing files.

Writing Files

# Writing (overwrites existing file)
    with open("output.txt", "w") as file:
        file.write("Hello, World!\n")
        file.write("Second line\n")
    
    # Appending (adds to existing file)
    with open("output.txt", "a") as file:
        file.write("Appended line\n")
    
    # Writing multiple lines
    lines = ["Line 1\n", "Line 2\n", "Line 3\n"]
    with open("output.txt", "w") as file:
        file.writelines(lines)

File Modes

Working with CSV Files

import csv
    
    # Reading CSV
    with open("data.csv", "r") as file:
        reader = csv.reader(file)
        for row in reader:
            print(row)  # Each row is a list
    
    # Reading CSV as dictionary
    with open("data.csv", "r") as file:
        reader = csv.DictReader(file)
        for row in reader:
            print(row['name'], row['age'])  # Access by column name
    
    # Writing CSV
    data = [
        ['Name', 'Age', 'City'],
        ['Alice', '30', 'NYC'],
        ['Bob', '25', 'LA']
    ]
    
    with open("output.csv", "w", newline='') as file:
        writer = csv.writer(file)
        writer.writerows(data)

Checking if File Exists

import os
    
    if os.path.exists("data.txt"):
        with open("data.txt", "r") as file:
            content = file.read()
    else:
        print("File not found!")
    
    # Check if path is file or directory
    print(os.path.isfile("data.txt"))  # True if file
    print(os.path.isdir("my_folder"))  # True if directory

Python List Comprehensions

List comprehensions provide an elegant way to create lists based on existing lists or ranges. They're more concise and often faster than traditional loops.

Basic List Comprehension

# Traditional way
    squares = []
    for x in range(10):
        squares.append(x ** 2)
    
    # List comprehension way
    squares = [x ** 2 for x in range(10)]
    # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    
    # String manipulation
    words = ['hello', 'world', 'python']
    uppercase = [word.upper() for word in words]
    # ['HELLO', 'WORLD', 'PYTHON']

List Comprehension with Conditions

# Only even numbers
    evens = [x for x in range(20) if x % 2 == 0]
    # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    
    # Only positive numbers
    numbers = [-2, -1, 0, 1, 2, 3]
    positives = [x for x in numbers if x > 0]
    # [1, 2, 3]
    
    # If-else in comprehension
    labels = ['even' if x % 2 == 0 else 'odd' for x in range(5)]
    # ['even', 'odd', 'even', 'odd', 'even']

Nested List Comprehensions

# Flattening a 2D list
    matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    flattened = [num for row in matrix for num in row]
    # [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
    # Creating multiplication table
    table = [[i * j for j in range(1, 6)] for i in range(1, 6)]
    # [[1, 2, 3, 4, 5], [2, 4, 6, 8, 10], ...]

Dictionary and Set Comprehensions

# Dictionary comprehension
    squares_dict = {x: x**2 for x in range(5)}
    # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
    
    # Set comprehension
    unique_lengths = {len(word) for word in ['hello', 'world', 'hi', 'bye']}
    # {2, 5} - unique lengths
    
    # Filtering dictionary
    prices = {'apple': 0.50, 'banana': 0.25, 'cherry': 0.75}
    expensive = {k: v for k, v in prices.items() if v > 0.30}
    # {'apple': 0.50, 'cherry': 0.75}

Python OOP (Classes and Objects)

Object-Oriented Programming (OOP) lets you model real-world concepts as objects with properties (attributes) and behaviors (methods). It's essential for building complex, maintainable programs.

Creating Your First Class

class Dog:
        def __init__(self, name, age):
            self.name = name  # Attribute
            self.age = age
        
        def bark(self):  # Method
            print(f"{self.name} says Woof!")
        
        def get_info(self):
            return f"{self.name} is {self.age} years old"
    
    # Creating objects (instances)
    dog1 = Dog("Buddy", 3)
    dog2 = Dog("Max", 5)
    
    dog1.bark()  # Buddy says Woof!
    print(dog2.get_info())  # Max is 5 years old

Understanding __init__ and self

__init__ is a special method called when creating a new object. self refers to the instance being created:

class Person:
        def __init__(self, name, age):
            # self.name creates an attribute called 'name'
            # The parameter 'name' provides the initial value
            self.name = name
            self.age = age
            self.greet_count = 0  # Can initialize attributes without parameters
        
        def greet(self):
            # self allows access to the instance's attributes
            self.greet_count += 1
            print(f"Hello, I'm {self.name}")
    
    person = Person("Alice", 30)
    person.greet()  # Hello, I'm Alice

Class vs Instance Attributes

class Dog:
        species = "Canis familiaris"  # Class attribute (shared by all)
        
        def __init__(self, name):
            self.name = name  # Instance attribute (unique to each)
    
    dog1 = Dog("Buddy")
    dog2 = Dog("Max")
    
    print(dog1.species)  # Canis familiaris
    print(dog2.species)  # Canis familiaris (same for all)
    
    print(dog1.name)  # Buddy
    print(dog2.name)  # Max (different for each)
    
    # Changing class attribute affects all instances
    Dog.species = "Canis lupus"
    print(dog1.species)  # Canis lupus
    print(dog2.species)  # Canis lupus

Properties and Encapsulation

class BankAccount:
        def __init__(self, balance):
            self._balance = balance  # _ indicates "private"
        
        @property
        def balance(self):
            return self._balance
        
        @balance.setter
        def balance(self, value):
            if value < 0:
                raise ValueError("Balance cannot be negative")
            self._balance = value
        
        def deposit(self, amount):
            if amount > 0:
                self._balance += amount
        
        def withdraw(self, amount):
            if amount > self._balance:
                print("Insufficient funds")
            else:
                self._balance -= amount
    
    account = BankAccount(1000)
    print(account.balance)  # Uses @property getter
    account.deposit(500)
    account.balance = 2000  # Uses @balance.setter

Inheritance and Polymorphism

Inheritance lets classes build upon existing classes, reusing and extending their functionality. Polymorphism allows objects of different classes to be used interchangeably.

Basic Inheritance

class Animal:
        def __init__(self, name):
            self.name = name
        
        def speak(self):
            print("Some generic animal sound")
    
    class Dog(Animal):  # Dog inherits from Animal
        def speak(self):  # Override parent method
            print(f"{self.name} says Woof!")
    
    class Cat(Animal):
        def speak(self):
            print(f"{self.name} says Meow!")
    
    # Using inherited classes
    dog = Dog("Buddy")
    cat = Cat("Whiskers")
    
    dog.speak()  # Buddy says Woof!
    cat.speak()  # Whiskers says Meow!

Calling Parent Methods

class Vehicle:
        def __init__(self, brand, model):
            self.brand = brand
            self.model = model
        
        def info(self):
            return f"{self.brand} {self.model}"
    
    class Car(Vehicle):
        def __init__(self, brand, model, doors):
            super().__init__(brand, model)  # Call parent __init__
            self.doors = doors
        
        def info(self):
            basic_info = super().info()  # Call parent info()
            return f"{basic_info} - {self.doors} doors"
    
    car = Car("Toyota", "Camry", 4)
    print(car.info())  # Toyota Camry - 4 doors

Multiple Inheritance

class Flyable:
        def fly(self):
            print("Flying!")
    
    class Swimmable:
        def swim(self):
            print("Swimming!")
    
    class Duck(Flyable, Swimmable):
        def quack(self):
            print("Quack!")
    
    duck = Duck()
    duck.fly()    # Flying!
    duck.swim()   # Swimming!
    duck.quack()  # Quack!

Polymorphism in Action

class Shape:
        def area(self):
            pass
    
    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height
        
        def area(self):
            return self.width * self.height
    
    class Circle(Shape):
        def __init__(self, radius):
            self.radius = radius
        
        def area(self):
            return 3.14159 * self.radius ** 2
    
    # Polymorphism: same method name, different behavior
    shapes = [Rectangle(5, 10), Circle(7), Rectangle(3, 4)]
    
    for shape in shapes:
        print(f"Area: {shape.area()}")  # Works for all shapes!

Python Virtual Environments

Virtual environments create isolated Python installations for each project. This prevents package conflicts between projects and makes your code portable.

Why Use Virtual Environments?

Creating a Virtual Environment

# Create virtual environment
    python -m venv myenv
    
    # Activate on Windows
    myenv\Scripts\activate
    
    # Activate on Mac/Linux
    source myenv/bin/activate
    
    # You'll see (myenv) in your terminal prompt when active

Installing Packages

# Install a package
    pip install requests
    
    # Install specific version
    pip install requests==2.28.0
    
    # Install multiple packages
    pip install requests numpy pandas
    
    # Uninstall package
    pip uninstall requests

Requirements File

# Save all installed packages to file
    pip freeze > requirements.txt
    
    # Install all packages from requirements file
    pip install -r requirements.txt
    
    # Example requirements.txt:
    # requests==2.28.0
    # numpy==1.24.0
    # pandas==1.5.0

Deactivating Virtual Environment

# Deactivate (returns to system Python)
    deactivate

Working with JSON and APIs

JSON (JavaScript Object Notation) is the standard format for data exchange on the web. Python's json module makes working with JSON data simple.

JSON Basics

import json
    
    # Python dictionary to JSON string
    person = {
        "name": "Alice",
        "age": 30,
        "city": "NYC",
        "hobbies": ["reading", "coding"]
    }
    
    json_string = json.dumps(person)
    print(json_string)
    # {"name": "Alice", "age": 30, "city": "NYC", "hobbies": ["reading", "coding"]}
    
    # Pretty printing JSON
    pretty_json = json.dumps(person, indent=4)
    print(pretty_json)
    
    # JSON string to Python dictionary
    json_data = '{"name": "Bob", "age": 25}'
    python_dict = json.loads(json_data)
    print(python_dict["name"])  # Bob

Reading and Writing JSON Files

# Writing JSON to file
    data = {
        "users": [
            {"name": "Alice", "age": 30},
            {"name": "Bob", "age": 25}
        ]
    }
    
    with open("data.json", "w") as file:
        json.dump(data, file, indent=4)
    
    # Reading JSON from file
    with open("data.json", "r") as file:
        loaded_data = json.load(file)
        print(loaded_data["users"][0]["name"])  # Alice

Making API Requests

import requests
    
    # GET request
    response = requests.get("https://api.github.com/users/python")
    data = response.json()  # Parse JSON response
    
    print(data["name"])
    print(data["public_repos"])
    
    # POST request with JSON data
    user_data = {
        "username": "newuser",
        "email": "user@example.com"
    }
    
    response = requests.post(
        "https://api.example.com/users",
        json=user_data,
        headers={"Authorization": "Bearer token123"}
    )
    
    if response.status_code == 201:
        print("User created successfully!")
    else:
        print(f"Error: {response.status_code}")

Handling API Errors

import requests
    
    try:
        response = requests.get("https://api.example.com/data", timeout=5)
        response.raise_for_status()  # Raises exception for 4xx/5xx status
        data = response.json()
        print(data)
    except requests.exceptions.Timeout:
        print("Request timed out")
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e}")
    except requests.exceptions.RequestException as e:
        print(f"Error: {e}")

Python Standard Library Overview

Python's standard library provides modules for common tasks without installing external packages. Here are the most useful ones:

Collections Module

from collections import Counter, defaultdict, namedtuple
    
    # Counter - count occurrences
    words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
    counts = Counter(words)
    print(counts)  # Counter({'apple': 3, 'banana': 2, 'cherry': 1})
    print(counts.most_common(2))  # [('apple', 3), ('banana', 2)]
    
    # defaultdict - dictionary with default values
    from collections import defaultdict
    grades = defaultdict(list)
    grades['Alice'].append(90)
    grades['Bob'].append(85)  # No KeyError if key doesn't exist
    
    # namedtuple - lightweight object
    Point = namedtuple('Point', ['x', 'y'])
    p = Point(10, 20)
    print(p.x, p.y)  # 10 20

itertools Module

from itertools import combinations, permutations, cycle, chain
    
    # Combinations
    items = ['A', 'B', 'C']
    for combo in combinations(items, 2):
        print(combo)  # ('A', 'B'), ('A', 'C'), ('B', 'C')
    
    # Cycle through items infinitely
    colors = cycle(['red', 'green', 'blue'])
    for i in range(5):
        print(next(colors))  # red, green, blue, red, green
    
    # Chain multiple iterables
    list1 = [1, 2, 3]
    list2 = [4, 5, 6]
    combined = list(chain(list1, list2))  # [1, 2, 3, 4, 5, 6]

pathlib Module (Modern File Paths)

from pathlib import Path
    
    # Create path object
    path = Path('data/files/document.txt')
    
    # Path operations
    print(path.exists())  # Check if exists
    print(path.is_file())  # Check if file
    print(path.parent)  # data/files
    print(path.name)  # document.txt
    print(path.suffix)  # .txt
    
    # Reading/writing with pathlib
    path.write_text("Hello, World!")
    content = path.read_text()
    
    # Iterate over directory
    data_dir = Path('data')
    for file in data_dir.glob('*.txt'):
        print(file)

Python Decorators Explained

Decorators modify the behavior of functions or classes without changing their source code. They're powerful tools for adding functionality like logging, timing, authentication, and caching.

Understanding Decorators

A decorator is a function that takes another function and extends its behavior:

def my_decorator(func):
        def wrapper():
            print("Before function call")
            func()
            print("After function call")
        return wrapper
    
    @my_decorator
    def say_hello():
        print("Hello!")
    
    say_hello()
    # Output:
    # Before function call
    # Hello!
    # After function call

The @my_decorator syntax is equivalent to:

def say_hello():
        print("Hello!")
    
    say_hello = my_decorator(say_hello)

Decorators with Arguments

def repeat(times):
        def decorator(func):
            def wrapper(*args, **kwargs):
                for _ in range(times):
                    result = func(*args, **kwargs)
                return result
            return wrapper
        return decorator
    
    @repeat(times=3)
    def greet(name):
        print(f"Hello, {name}!")
    
    greet("Alice")
    # Hello, Alice!
    # Hello, Alice!
    # Hello, Alice!

Practical Decorator Examples

import time
    from functools import wraps
    
    # Timing decorator
    def timer(func):
        @wraps(func)  # Preserves original function's metadata
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            print(f"{func.__name__} took {end - start:.4f} seconds")
            return result
        return wrapper
    
    @timer
    def slow_function():
        time.sleep(2)
        return "Done"
    
    # Caching decorator
    def cache(func):
        cached_results = {}
        
        @wraps(func)
        def wrapper(*args):
            if args in cached_results:
                print("Using cached result")
                return cached_results[args]
            result = func(*args)
            cached_results[args] = result
            return result
        return wrapper
    
    @cache
    def fibonacci(n):
        if n < 2:
            return n
        return fibonacci(n-1) + fibonacci(n-2)

Class-based Decorators

class CountCalls:
        def __init__(self, func):
            self.func = func
            self.count = 0
        
        def __call__(self, *args, **kwargs):
            self.count += 1
            print(f"Call {self.count} to {self.func.__name__}")
            return self.func(*args, **kwargs)
    
    @CountCalls
    def say_hello():
        print("Hello!")
    
    say_hello()  # Call 1 to say_hello
    say_hello()  # Call 2 to say_hello

Python Generators and Iterators

Generators produce values on the-fly instead of storing them in memory. They're perfect for working with large datasets or infinite sequences.

Creating Generators

# Generator function (uses yield instead of return)
    def count_up_to(n):
        count = 1
        while count <= n:
            yield count
            count += 1
    
    # Using the generator
    for num in count_up_to(5):
        print(num)  # 1, 2, 3, 4, 5
    
    # Generator is exhausted after one iteration
    counter = count_up_to(3)
    print(list(counter))  # [1, 2, 3]
    print(list(counter))  # [] - empty, already consumed

Generator Expressions

# List comprehension (creates entire list in memory)
    squares_list = [x**2 for x in range(1000000)]  # Takes lots of memory!
    
    # Generator expression (creates values on demand)
    squares_gen = (x**2 for x in range(1000000))  # Very memory efficient
    
    # Using generator
    for square in squares_gen:
        if square > 100:
            break
        print(square)

Practical Generator Examples

# Reading large files efficiently
    def read_large_file(file_path):
        with open(file_path, 'r') as file:
            for line in file:
                yield line.strip()
    
    # Process one line at a time (memory efficient)
    for line in read_large_file('huge_file.txt'):
        process(line)
    
    # Infinite sequence generator
    def fibonacci():
        a, b = 0, 1
        while True:
            yield a
            a, b = b, a + b
    
    # Generate first 10 Fibonacci numbers
    fib = fibonacci()
    for _ in range(10):
        print(next(fib))

Creating Custom Iterators

class Countdown:
        def __init__(self, start):
            self.current = start
        
        def __iter__(self):
            return self
        
        def __next__(self):
            if self.current <= 0:
                raise StopIteration
            self.current -= 1
            return self.current + 1
    
    for num in Countdown(5):
        print(num)  # 5, 4, 3, 2, 1

Python Context Managers

Context managers handle resource management automatically perfect for files, database connections, and locks. The with statement ensures cleanup happens even if errors occur.

Creating Context Managers

# Using class
    class FileHandler:
        def __init__(self, filename, mode):
            self.filename = filename
            self.mode = mode
            self.file = None
        
        def __enter__(self):
            self.file = open(self.filename, self.mode)
            return self.file
        
        def __exit__(self, exc_type, exc_val, exc_tb):
            if self.file:
                self.file.close()
            # Return False to propagate exceptions
            return False
    
    with FileHandler('data.txt', 'w') as file:
        file.write("Hello, World!")
    # File automatically closed when leaving 'with' block

Using contextlib

from contextlib import contextmanager
    
    @contextmanager
    def timer():
        import time
        start = time.time()
        try:
            yield
        finally:
            end = time.time()
            print(f"Elapsed time: {end - start:.4f} seconds")
    
    with timer():
        # Code to time
        sum([i**2 for i in range(1000000)])

Python Multithreading vs Multiprocessing

Python offers two approaches for concurrent execution: threads for I/O-bound tasks and processes for CPU-bound tasks.

Threading (I/O-Bound Tasks)

import threading
    import time
    
    def download_file(file_id):
        print(f"Starting download {file_id}")
        time.sleep(2)  # Simulate I/O operation
        print(f"Finished download {file_id}")
    
    # Sequential (slow)
    start = time.time()
    for i in range(5):
        download_file(i)
    print(f"Sequential: {time.time() - start:.2f}s")  # ~10 seconds
    
    # Concurrent with threads (fast)
    start = time.time()
    threads = []
    for i in range(5):
        thread = threading.Thread(target=download_file, args=(i,))
        threads.append(thread)
        thread.start()
    
    for thread in threads:
        thread.join()  # Wait for all threads to complete
    print(f"Threaded: {time.time() - start:.2f}s")  # ~2 seconds

Multiprocessing (CPU-Bound Tasks)

from multiprocessing import Pool
    import time
    
    def cpu_intensive_task(n):
        """Simulate CPU-intensive work"""
        count = 0
        for i in range(n):
            count += i ** 2
        return count
    
    # Sequential
    start = time.time()
    results = [cpu_intensive_task(10000000) for _ in range(4)]
    print(f"Sequential: {time.time() - start:.2f}s")
    
    # Parallel with multiprocessing
    start = time.time()
    with Pool(processes=4) as pool:
        results = pool.map(cpu_intensive_task, [10000000] * 4)
    print(f"Parallel: {time.time() - start:.2f}s")
Thread vs Process:
- Threads: Share memory, lightweight, good for I/O (network, files)
- Processes: Separate memory, heavier, good for CPU-intensive tasks
- Python's GIL limits thread performance for CPU tasks

Asynchronous Python (async / await)

Async programming lets you write concurrent code that handles many operations without blocking. It's perfect for web servers, APIs, and I/O-heavy applications.

Basic Async/Await

import asyncio
    
    async def fetch_data(id):
        print(f"Fetching {id}...")
        await asyncio.sleep(2)  # Simulate async I/O
        print(f"Done {id}")
        return f"Data {id}"
    
    async def main():
        # Run tasks concurrently
        results = await asyncio.gather(
            fetch_data(1),
            fetch_data(2),
            fetch_data(3)
        )
        print(results)
    
    # Run async code
    asyncio.run(main())  # Takes ~2 seconds, not 6!

Async HTTP Requests

import aiohttp
    import asyncio
    
    async def fetch_url(session, url):
        async with session.get(url) as response:
            return await response.text()
    
    async def fetch_multiple_urls():
        urls = [
            'https://api.github.com/users/python',
            'https://api.github.com/users/google',
            'https://api.github.com/users/microsoft'
        ]
        
        async with aiohttp.ClientSession() as session:
            tasks = [fetch_url(session, url) for url in urls]
            results = await asyncio.gather(*tasks)
            return results
    
    results = asyncio.run(fetch_multiple_urls())

Python Performance Optimization

Writing fast Python code requires understanding bottlenecks and choosing the right tools. Here are proven optimization techniques.

Use Built in Functions and Libraries

# Slow: Manual implementation
    def sum_squares_slow(numbers):
        total = 0
        for num in numbers:
            total += num ** 2
        return total
    
    # Fast: Built-in functions
    def sum_squares_fast(numbers):
        return sum(num ** 2 for num in numbers)
    
    # Even faster: NumPy for large datasets
    import numpy as np
    def sum_squares_numpy(numbers):
        arr = np.array(numbers)
        return np.sum(arr ** 2)

List Comprehensions vs Loops

import time
    
    numbers = range(1000000)
    
    # Slower: append in loop
    start = time.time()
    result = []
    for num in numbers:
        result.append(num ** 2)
    print(f"Loop: {time.time() - start:.4f}s")
    
    # Faster: list comprehension
    start = time.time()
    result = [num ** 2 for num in numbers]
    print(f"Comprehension: {time.time() - start:.4f}s")

Avoid Repeated Calculations

# Slow: repeated calculation
    def slow_function(items):
        for item in items:
            if item in expensive_calculation():  # Called every iteration!
                process(item)
    
    # Fast: calculate once
    def fast_function(items):
        valid_items = expensive_calculation()  # Called once
        for item in items:
            if item in valid_items:
                process(item)

Use Local Variables

# Slower: global lookup
    import math
    
    def calculate():
        result = 0
        for i in range(1000000):
            result += math.sqrt(i)  # Looks up 'math' globally each time
    
    # Faster: local variable
    def calculate_fast():
        result = 0
        sqrt = math.sqrt  # Local reference
        for i in range(1000000):
            result += sqrt(i)

Python Memory Management

Understanding how Python manages memory helps you write more efficient code and avoid memory leaks.

Reference Counting

import sys
    
    x = []  # Creates list, reference count = 1
    y = x   # Same object, reference count = 2
    
    print(sys.getrefcount(x))  # Shows reference count
    
    del y   # Decreases reference count
    # When reference count reaches 0, object is freed

Garbage Collection

import gc
    
    # Check garbage collection status
    print(gc.isenabled())  # True
    
    # Manual garbage collection
    gc.collect()  # Force collection
    
    # View garbage collection stats
    print(gc.get_stats())

Memory Efficient Data Structures

# Generators instead of lists for large sequences
    def large_range(n):
        for i in range(n):
            yield i
    
    # Use __slots__ to reduce memory in classes
    class Point:
        __slots__ = ['x', 'y']  # No __dict__, less memory
        
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    # Tuples use less memory than lists
    import sys
    print(sys.getsizeof([1, 2, 3]))  # More bytes
    print(sys.getsizeof((1, 2, 3)))  # Fewer bytes

Python Security Best Practices

Security vulnerabilities can expose your users and systems to attacks. Follow these practices to write secure Python code.

Never Use eval() with User Input

# DANGEROUS - arbitrary code execution
    user_input = input("Enter expression: ")
    result = eval(user_input)  # User could enter: __import__('os').system('rm -rf /')
    
    # SAFE - use ast.literal_eval for data
    import ast
    user_input = "[1, 2, 3]"
    result = ast.literal_eval(user_input)  # Only evaluates literals

SQL Injection Prevention

import sqlite3
    
    # DANGEROUS - SQL injection vulnerable
    username = input("Username: ")
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)  # User could enter: admin' OR '1'='1
    
    # SAFE - parameterized queries
    username = input("Username: ")
    query = "SELECT * FROM users WHERE username = ?"
    cursor.execute(query, (username,))

Secure Password Storage

import hashlib
    import os
    
    # NEVER store plain text passwords!
    # BAD: password = "user_password"
    
    # GOOD: Hash with salt
    def hash_password(password):
        salt = os.urandom(32)  # Random salt
        key = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt,
            100000  # Iterations
        )
        return salt + key
    
    def verify_password(stored_password, provided_password):
        salt = stored_password[:32]
        stored_key = stored_password[32:]
        new_key = hashlib.pbkdf2_hmac(
            'sha256',
            provided_password.encode('utf-8'),
            salt,
            100000
        )
        return new_key == stored_key

Input Validation

import re
    
    def validate_email(email):
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
        return re.match(pattern, email) is not None
    
    def validate_age(age):
        try:
            age_int = int(age)
            return 0 <= age_int <= 150
        except ValueError:
            return False
    
    # Always validate and sanitize user input
    email = input("Email: ")
    if not validate_email(email):
        print("Invalid email format")

Writing Clean & Maintainable Python Code

Clean code is easier to read, debug, and maintain. Follow these principles to write professional Python.

PEP 8 Style Guide

# Good naming
    user_name = "Alice"  # snake_case for variables
    MAX_SIZE = 100       # UPPERCASE for constants
    
    class UserAccount:   # PascalCase for classes
        pass
    
    def calculate_total():  # snake_case for functions
        pass
    
    # Good spacing
    def function_name(param1, param2):
        result = param1 + param2  # Spaces around operators
        return result
    
    # Line length: max 79 characters
    long_string = (
        "This is a very long string that needs to be "
        "split across multiple lines for readability"
    )

Meaningful Names

# Bad: cryptic names
    d = {}
    t = 0
    for x in lst:
        t += x
    
    # Good: descriptive names
    user_scores = {}
    total_score = 0
    for score in score_list:
        total_score += score

DRY Principle (Don't Repeat Yourself)

# Bad: repeated code
    user1_total = user1_price * user1_quantity
    user2_total = user2_price * user2_quantity
    user3_total = user3_price * user3_quantity
    
    # Good: function for repeated logic
    def calculate_total(price, quantity):
        return price * quantity
    
    user1_total = calculate_total(user1_price, user1_quantity)
    user2_total = calculate_total(user2_price, user2_quantity)
    user3_total = calculate_total(user3_price, user3_quantity)

Documentation

def calculate_compound_interest(principal, rate, time, compounds_per_year):
        """
        Calculate compound interest.
        
        Args:
            principal (float): Initial investment amount
            rate (float): Annual interest rate (as decimal, e.g., 0.05 for 5%)
            time (int): Time period in years
            compounds_per_year (int): Number of times interest compounds per year
        
        Returns:
            float: Final amount after compound interest
        
        Example:
            >>> calculate_compound_interest(1000, 0.05, 10, 4)
            1643.62
        """
        amount = principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)
        return round(amount, 2)

Common Python Bugs and How to Fix Them

Bug #1: Mutable Default Arguments

# Bug
    def add_item(item, items=[]):
        items.append(item)
        return items
    
    print(add_item(1))  # [1]
    print(add_item(2))  # [1, 2] - UNEXPECTED!
    
    # Fix
    def add_item(item, items=None):
        if items is None:
            items = []
        items.append(item)
        return items

Bug #2: Late Binding Closures

# Bug
    functions = []
    for i in range(3):
        functions.append(lambda: i)
    
    for f in functions:
        print(f())  # All print 2!
    
    # Fix
    functions = []
    for i in range(3):
        functions.append(lambda i=i: i)
    
    for f in functions:
        print(f())  # Prints 0, 1, 2

Bug #3: Modifying List During Iteration

# Bug
    numbers = [1, 2, 3, 4, 5]
    for num in numbers:
        if num % 2 == 0:
            numbers.remove(num)  # Skips elements!
    
    # Fix
    numbers = [1, 2, 3, 4, 5]
    numbers = [num for num in numbers if num % 2 != 0]

Building Real-World Python Projects

Let's build a complete command line todo application that demonstrates everything you've learned:

import json
    from datetime import datetime
    from pathlib import Path
    
    class TodoApp:
        def __init__(self, filename='todos.json'):
            self.filename = filename
            self.tasks = self.load_tasks()
        
        def load_tasks(self):
            path = Path(self.filename)
            if path.exists():
                with open(path, 'r') as f:
                    return json.load(f)
            return []
        
        def save_tasks(self):
            with open(self.filename, 'w') as f:
                json.dump(self.tasks, f, indent=4)
        
        def add_task(self, description, priority='medium'):
            task = {
                'id': len(self.tasks) + 1,
                'description': description,
                'priority': priority,
                'completed': False,
                'created': datetime.now().isoformat()
            }
            self.tasks.append(task)
            self.save_tasks()
            print(f"✓ Task added: {description}")
        
        def complete_task(self, task_id):
            for task in self.tasks:
                if task['id'] == task_id:
                    task['completed'] = True
                    self.save_tasks()
                    print(f"✓ Task completed: {task['description']}")
                    return
            print("Task not found")
        
        def list_tasks(self, show_completed=False):
            if not self.tasks:
                print("No tasks yet!")
                return
            
            print("\n=== Your Tasks ===")
            for task in self.tasks:
                if task['completed'] and not show_completed:
                    continue
                
                status = "✓" if task['completed'] else " "
                priority_icon = {
                    'high': '🔴',
                    'medium': '🟡',
                    'low': '🟢'
                }.get(task['priority'], '⚪')
                
                print(f"[{status}] {task['id']}. {priority_icon} {task['description']}")
        
        def delete_task(self, task_id):
            self.tasks = [t for t in self.tasks if t['id'] != task_id]
            self.save_tasks()
            print(f"✓ Task deleted")
    
    def main():
        app = TodoApp()
        
        while True:
            print("\n=== Todo App ===")
            print("1. Add task")
            print("2. List tasks")
            print("3. Complete task")
            print("4. Delete task")
            print("5. Exit")
            
            choice = input("\nChoice: ").strip()
            
            if choice == '1':
                desc = input("Task description: ")
                priority = input("Priority (high/medium/low): ").lower() or 'medium'
                app.add_task(desc, priority)
            
            elif choice == '2':
                show_all = input("Show completed? (y/n): ").lower() == 'y'
                app.list_tasks(show_all)
            
            elif choice == '3':
                app.list_tasks()
                task_id = int(input("Task ID to complete: "))
                app.complete_task(task_id)
            
            elif choice == '4':
                app.list_tasks()
                task_id = int(input("Task ID to delete: "))
                app.delete_task(task_id)
            
            elif choice == '5':
                print("Goodbye!")
                break
    
    if __name__ == '__main__':
        main()

🎯 Project Ideas to Build Your Skills

  1. Password Manager - Practice file I/O, encryption, OOP
  2. Web Scraper - Learn requests, BeautifulSoup, data processing
  3. API Wrapper - Practice async programming, error handling
  4. CLI Tool - Master argparse, file operations, user interaction
  5. Data Analyzer - Work with pandas, visualization, statistics
  6. Chat Bot - Combine NLP libraries, OOP, external APIs
  7. Automation Script - File management, scheduled tasks, notifications

Your Python Journey: Next Steps

Congratulations! You've completed a comprehensive Python course covering beginner, intermediate, and advanced topics. You now understand:

Continue Learning

Frameworks to Explore:

Learning Resources:

The Path Forward

The best way to master Python is to build real projects. Don't just read code. Make mistakes. Debug them. Read other people's code. Contribute to open source. Each line you write makes you a better developer.

Remember: every expert was once a beginner. The Python community is welcoming and helpful. Don't be afraid to ask questions, share your work, and help others when you can.

Final Advice from a Senior Developer

Write code every day. Even 30 minutes of consistent practice beats occasional marathon sessions. Build small projects that solve real problems in your life. Automate boring tasks. Create tools you'll actually use.

Read code as much as you write it. Study popular open source projects. See how experienced developers structure their code, handle errors, and write documentation.

Embrace debugging. Errors aren't failures they're learning opportunities. Each bug you fix teaches you something new about how Python works.

Stay curious. Python is constantly evolving. New libraries, tools, and best practices emerge regularly. Keep learning, keep experimenting, and most importantly keep coding.

🐍 You're now a Python developer!

You have the knowledge. You have the tools. Now go build something amazing.
The Python community welcomes you. Happy coding! 🚀