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:
- Beginner-Friendly - You can write working programs on your first day. No other major language makes this as easy.
- Versatile - Build websites, analyze data, create games, automate tasks, or develop AI applications—all with one language.
- In-Demand - Companies from startups to Google use Python. It's consistently among the highest paying programming skills.
- Massive Community - Millions of Python developers means you'll find help, libraries, and tutorials for virtually anything.
- Free and Open Source - You'll never pay for Python or its ecosystem of tools.
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:
- You write code in a
.pyfile using plain text - Python interpreter reads your code line by line
- The interpreter translates each instruction into machine code
- Your computer executes the machine code
- 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.
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:
- Web Applications - Instagram, Spotify, Netflix, and YouTube all run on Python backends
- Data Analysis - Scientists and analysts use Python to process everything from climate data to stock markets
- Artificial Intelligence - Most machine learning and AI tools are built with Python (TensorFlow, PyTorch, etc.)
- Automation Scripts - Automate boring tasks like renaming files, scraping websites, or sending emails
- Games - While not ideal for AAA games, Python powers many indie games and game development tools
- Scientific Computing - NASA uses Python for calculations and data processing
- Desktop Applications - Dropbox's desktop client is written in Python
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
- Go to python.org (the official Python website)
- Click the Downloads tab
- Click the big yellow button that says Download Python 3.x.x (the numbers will be the latest version)
- Run the downloaded installer
- CRITICAL: Check the box that says "Add Python to PATH" at the bottom of the installer
- Click "Install Now"
- Wait for installation to complete
Installing Python on Mac
Macs come with Python 2.7 pre-installed, but remember we want Python 3:
- Go to python.org/downloads
- Download the latest Python 3 installer for macOS
- Open the downloaded
.pkgfile - Follow the installation wizard
- 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:
- Open your terminal/command prompt
- Type
python --version(orpython3 --versionon Mac/Linux) - 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)
- Free, modern, and powerful
- Download from code.visualstudio.com
- Install the Python extension for syntax highlighting and debugging
- Works on Windows, Mac, and Linux
PyCharm Community Edition
- Built specifically for Python
- Excellent for larger projects
- Can feel heavy for beginners
- Download from jetbrains.com/pycharm
IDLE (Comes with Python)
- Already installed when you installed Python
- Basic but functional
- Great for quick tests and learning
- Find it in your programs menu as "IDLE"
Your First Python Program
Let's write your first program to confirm everything works:
- Open your chosen editor
- Create a new file called
hello.py - Type this exact code:
print("Hello, Python!")
- Save the file
- Open your terminal/command prompt
- Navigate to where you saved the file
- Run:
python hello.py
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!")
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:
- Must start with a letter or underscore (a-z, A-Z, _)
- Can contain letters, numbers, and underscores
- Cannot start with a number
- Cannot use Python keywords (like
if,for,while) - Are case-sensitive (
ageandAgeare different)
# 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!
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")
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!")
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}")
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...")
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}
- 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
🎓 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
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)
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
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
'r'- Read (default) - File must exist'w'- Write - Creates file or overwrites existing'a'- Append - Creates file or appends to existing'r+'- Read and write'b'- Binary mode (add to any mode: 'rb', 'wb')
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?
- Different projects can use different package versions
- Keeps your system Python clean
- Makes projects portable and reproducible
- Required for deploying applications
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)
ADVANCED
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")
- 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
- Password Manager - Practice file I/O, encryption, OOP
- Web Scraper - Learn requests, BeautifulSoup, data processing
- API Wrapper - Practice async programming, error handling
- CLI Tool - Master argparse, file operations, user interaction
- Data Analyzer - Work with pandas, visualization, statistics
- Chat Bot - Combine NLP libraries, OOP, external APIs
- 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:
- Python fundamentals: variables, data types, control flow
- Functions, modules, and code organization
- Object-oriented programming and design patterns
- File handling, APIs, and data processing
- Advanced concepts: decorators, generators, async programming
- Performance optimization and memory management
- Security best practices and clean code principles
- Debugging strategies and common pitfalls
Continue Learning
Frameworks to Explore:
- Django/Flask - Web development
- FastAPI - Modern APIs
- Pandas/NumPy - Data science
- Pytest - Testing
- SQLAlchemy - Databases
Learning Resources:
- Official Python documentation (docs.python.org)
- Real Python tutorials
- Python Package Index (PyPI.org)
- GitHub open source projects
- Stack Overflow for problem-solving
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! 🚀