Python Fundamentals: Complete Programming Foundation
Build a solid foundation in Python programming with comprehensive coverage of syntax.
Introduction and Setup
Python has earned its reputation as one of the most approachable programming languages. What takes 20 lines of code in other languages often requires just 3 lines in Python. This simplicity isn’t accidental—it’s the result of deliberate design choices that prioritize readability and developer productivity.
When Guido van Rossum created Python, he built it around a philosophy of clean, readable code. The result is a language that reads almost like English, making it perfect for beginners while remaining powerful enough for complex applications. This guide will take you from Python basics to building real-world applications.
Why Python Matters
I’ve used Python for web development, data analysis, automation scripts, machine learning models, and even game development. This versatility is Python’s superpower. You can learn one language and apply it to virtually any programming domain.
The job market reflects this versatility. Python consistently ranks among the most in-demand programming languages. Whether you’re interested in web development, data science, DevOps, or artificial intelligence, Python skills open doors.
Getting Python Running
The easiest way to get started is installing Python from python.org. I recommend Python 3.9 or later - Python 2 is officially dead, so don’t bother with it.
On Windows: Download the installer from python.org and make sure to check “Add Python to PATH” during installation. This lets you run Python from any command prompt.
On macOS: macOS comes with Python, but it’s usually an older version. Install a fresh copy:
# Using Homebrew (recommended)
brew install python
# Verify installation
python3 --version
On Linux: Most distributions include Python, but you might need to install pip separately:
# Ubuntu/Debian
sudo apt update
sudo apt install python3 python3-pip
# Verify installation
python3 --version
pip3 --version
Your First Python Experience
Open a terminal and type python3
(or just python
on Windows). You’ll see the Python interpreter prompt:
>>> print("Hello, World!")
Hello, World!
>>> 2 + 2
4
>>> name = "Python"
>>> f"I love {name}!"
'I love Python!'
This interactive mode is perfect for experimenting. I still use it daily to test ideas and debug code.
Setting Up a Development Environment
While you can write Python in any text editor, a good development environment makes coding much more enjoyable. Here are my recommendations:
VS Code (My favorite): Free, lightweight, with excellent Python support. Install the Python extension for syntax highlighting, debugging, and IntelliSense.
PyCharm: Full-featured IDE with powerful debugging and refactoring tools. The Community Edition is free and perfect for learning.
Jupyter Notebooks: Essential for data science and experimentation. Install with:
pip install jupyter
jupyter notebook
Virtual Environments
This is crucial: always use virtual environments for your projects. They prevent dependency conflicts and keep your system Python clean.
# Create a virtual environment
python3 -m venv myproject
# Activate it (Linux/macOS)
source myproject/bin/activate
# Activate it (Windows)
myproject\Scripts\activate
# Install packages
pip install requests
# Deactivate when done
deactivate
I learned this lesson the hard way after breaking my system Python by installing conflicting packages globally. Virtual environments save you from that pain.
Package Management with pip
Python’s package ecosystem is massive. The Python Package Index (PyPI) contains over 400,000 packages. You’ll install them using pip:
# Install a package
pip install requests
# Install specific version
pip install django==4.2.0
# Install from requirements file
pip install -r requirements.txt
# List installed packages
pip list
# Show package info
pip show requests
Always create a requirements.txt file for your projects:
pip freeze > requirements.txt
This lets others recreate your exact environment.
Your First Real Program
Let’s write something more interesting than “Hello, World!”:
# weather.py
import requests
def get_weather(city):
"""Get current weather for a city"""
api_key = "your_api_key_here"
url = f"http://api.openweathermap.org/data/2.5/weather"
params = {
'q': city,
'appid': api_key,
'units': 'metric'
}
response = requests.get(url, params=params)
data = response.json()
return f"{city}: {data['main']['temp']}°C, {data['weather'][0]['description']}"
if __name__ == "__main__":
city = input("Enter city name: ")
print(get_weather(city))
This program demonstrates several Python concepts: functions, string formatting, dictionaries, API calls, and the if __name__ == "__main__"
pattern.
Common Beginner Mistakes
I’ve seen these mistakes countless times (and made them myself):
Forgetting to activate virtual environments. Always check your prompt shows the environment name.
Using Python 2 syntax in Python 3. Print statements, integer division, and string handling changed between versions.
Not reading error messages. Python’s error messages are usually helpful - read them carefully.
Mixing tabs and spaces. Use spaces for indentation (4 spaces is the standard).
What’s Next
You now have Python installed and understand the basics of the development environment. This foundation will serve you well as we dive into Python’s core concepts.
The beauty of Python is that you can start simple and gradually add complexity. Every expert Python developer started exactly where you are now.
Next, we’ll explore Python’s core concepts including variables, data types, and control structures that form the building blocks of every Python program.
Core Concepts and Fundamentals
The moment Python clicked for me was when I realized I didn’t need to declare variable types. Coming from Java where you write String name = "John"
, Python’s simple name = "John"
felt like magic. This dynamic typing is one of Python’s most powerful features, but it also trips up many beginners.
Understanding Python’s core concepts - variables, data types, and control structures - is like learning the alphabet before writing sentences. These fundamentals appear in every Python program you’ll ever write.
Variables and Dynamic Typing
In Python, variables are just names that point to objects. You don’t declare types - Python figures them out:
# Python infers types automatically
name = "Alice" # string
age = 30 # integer
height = 5.6 # float
is_student = True # boolean
# Variables can change types
score = 100 # integer
score = "A+" # now it's a string
This flexibility is powerful but can cause confusion. I always use descriptive variable names to make the intended type obvious:
# Good: clear intent
user_count = 42
average_temperature = 23.5
is_logged_in = False
# Bad: unclear intent
x = 42
temp = 23.5
flag = False
Essential Data Types
Python has several built-in data types you’ll use constantly:
Numbers:
# Integers - whole numbers
count = 10
negative = -5
# Floats - decimal numbers
price = 19.99
pi = 3.14159
# Operations
result = 10 + 5 * 2 # 20 (multiplication first)
division = 10 / 3 # 3.3333... (float division)
floor_div = 10 // 3 # 3 (integer division)
remainder = 10 % 3 # 1 (modulo)
power = 2 ** 3 # 8 (exponentiation)
Strings:
# Different ways to create strings
single = 'Hello'
double = "World"
multiline = """This is a
long string that spans
multiple lines"""
# String operations
greeting = "Hello" + " " + "World" # Concatenation
repeated = "Ha" * 3 # "HaHaHa"
length = len("Python") # 6
# String methods (there are many!)
text = " Python Programming "
clean = text.strip() # Remove whitespace
upper = text.upper() # Convert to uppercase
words = text.split() # Split into list
Booleans:
# Boolean values
is_valid = True
is_empty = False
# Boolean operations
result = True and False # False
result = True or False # True
result = not True # False
# Comparison operators
age = 25
is_adult = age >= 18 # True
is_teenager = 13 <= age < 20 # False
Collections: Lists, Tuples, and Dictionaries
Python’s collection types are where the language really shines:
Lists - Ordered, Mutable Collections:
# Creating lists
fruits = ["apple", "banana", "orange"]
numbers = [1, 2, 3, 4, 5]
mixed = ["hello", 42, True, 3.14]
# List operations
fruits.append("grape") # Add to end
fruits.insert(0, "mango") # Insert at position
first_fruit = fruits[0] # Access by index
last_fruit = fruits[-1] # Negative indexing
some_fruits = fruits[1:3] # Slicing
# List methods
fruits.remove("banana") # Remove by value
popped = fruits.pop() # Remove and return last
fruits.sort() # Sort in place
Tuples - Ordered, Immutable Collections:
# Tuples use parentheses
coordinates = (10, 20)
rgb_color = (255, 128, 0)
# Tuple unpacking (very useful!)
x, y = coordinates
red, green, blue = rgb_color
# Tuples are immutable
# coordinates[0] = 15 # This would cause an error
Dictionaries - Key-Value Pairs:
# Creating dictionaries
person = {
"name": "Alice",
"age": 30,
"city": "New York"
}
# Dictionary operations
name = person["name"] # Access by key
person["email"] = "[email protected]" # Add new key
person["age"] = 31 # Update existing key
# Safe access
email = person.get("email", "No email") # Returns default if key missing
# Dictionary methods
keys = person.keys() # Get all keys
values = person.values() # Get all values
items = person.items() # Get key-value pairs
Control Structures
Control structures determine how your program flows:
Conditional Statements:
age = 20
if age < 13:
category = "child"
elif age < 20:
category = "teenager"
else:
category = "adult"
# Ternary operator (one-liner)
status = "minor" if age < 18 else "adult"
Loops:
# For loops - iterate over sequences
fruits = ["apple", "banana", "orange"]
for fruit in fruits:
print(f"I like {fruit}")
# Range function for numbers
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(1, 6): # 1, 2, 3, 4, 5
print(i)
# While loops - continue until condition is false
count = 0
while count < 5:
print(f"Count: {count}")
count += 1
# Loop control
for i in range(10):
if i == 3:
continue # Skip this iteration
if i == 7:
break # Exit the loop
print(i)
Functions - Your First Abstraction
Functions let you organize code into reusable blocks:
def greet(name):
"""Return a greeting message"""
return f"Hello, {name}!"
# Function with default parameters
def calculate_area(length, width=1):
"""Calculate rectangle area"""
return length * width
# Function with multiple return values
def get_name_parts(full_name):
"""Split full name into parts"""
parts = full_name.split()
first_name = parts[0]
last_name = parts[-1] if len(parts) > 1 else ""
return first_name, last_name
# Using functions
message = greet("Alice")
area = calculate_area(5, 3) # 15
square_area = calculate_area(4) # 4 (width defaults to 1)
first, last = get_name_parts("John Doe")
String Formatting
Python has several ways to format strings. I prefer f-strings for their readability:
name = "Alice"
age = 30
score = 95.67
# f-strings (Python 3.6+) - my favorite
message = f"Hello {name}, you are {age} years old"
formatted = f"Score: {score:.1f}%" # One decimal place
# .format() method
message = "Hello {}, you are {} years old".format(name, age)
message = "Hello {name}, you are {age} years old".format(name=name, age=age)
# % formatting (older style)
message = "Hello %s, you are %d years old" % (name, age)
Error Handling Basics
Errors happen. Python’s try/except blocks help you handle them gracefully:
# Basic error handling
try:
number = int(input("Enter a number: "))
result = 10 / number
print(f"Result: {result}")
except ValueError:
print("That's not a valid number!")
except ZeroDivisionError:
print("Cannot divide by zero!")
except Exception as e:
print(f"Something went wrong: {e}")
List Comprehensions - Python’s Secret Weapon
List comprehensions let you create lists in a concise, readable way:
# Traditional approach
squares = []
for i in range(10):
squares.append(i ** 2)
# List comprehension (more Pythonic)
squares = [i ** 2 for i in range(10)]
# With conditions
even_squares = [i ** 2 for i in range(10) if i % 2 == 0]
# Processing existing lists
names = ["alice", "bob", "charlie"]
capitalized = [name.capitalize() for name in names]
Common Patterns
These patterns appear in almost every Python program:
# Checking if something exists
if "apple" in fruits:
print("We have apples!")
# Iterating with index
for i, fruit in enumerate(fruits):
print(f"{i}: {fruit}")
# Iterating over dictionary
for key, value in person.items():
print(f"{key}: {value}")
# Multiple assignment
a, b = 1, 2
a, b = b, a # Swap values
These core concepts form the foundation of every Python program. Master them, and you’ll be able to read and write most Python code. The beauty is in their simplicity - Python’s syntax stays out of your way so you can focus on solving problems.
Next, we’ll put these concepts to work with practical applications and examples that show how these fundamentals combine to create useful programs.
Practical Applications and Examples
The best way to learn programming is by building things that solve real problems. I remember my first useful Python script - a simple file organizer that sorted my messy Downloads folder. It wasn’t elegant, but it worked, and more importantly, it saved me hours of manual work.
That’s the beauty of Python: you can quickly go from learning syntax to building tools that make your life easier. Let’s explore practical applications that demonstrate how Python’s fundamentals combine to solve real-world problems.
File and Directory Operations
One of Python’s strengths is working with files and directories. Here’s a practical file organizer:
import os
import shutil
from pathlib import Path
def organize_downloads():
"""Organize files in Downloads folder by extension"""
downloads = Path.home() / "Downloads"
# File type mappings
file_types = {
'images': ['.jpg', '.jpeg', '.png', '.gif', '.bmp'],
'documents': ['.pdf', '.doc', '.docx', '.txt', '.rtf'],
'videos': ['.mp4', '.avi', '.mkv', '.mov', '.wmv'],
'music': ['.mp3', '.wav', '.flac', '.aac'],
'archives': ['.zip', '.rar', '.7z', '.tar', '.gz']
}
# Create folders if they don't exist
for folder in file_types.keys():
folder_path = downloads / folder
folder_path.mkdir(exist_ok=True)
# Organize files
for file_path in downloads.iterdir():
if file_path.is_file():
extension = file_path.suffix.lower()
# Find the right category
for category, extensions in file_types.items():
if extension in extensions:
destination = downloads / category / file_path.name
shutil.move(str(file_path), str(destination))
print(f"Moved {file_path.name} to {category}/")
break
if __name__ == "__main__":
organize_downloads()
This script demonstrates file operations, dictionaries, loops, and the powerful pathlib
module.
Web Scraping and APIs
Python excels at gathering data from the web. Here’s a weather checker using a public API:
import requests
import json
class WeatherChecker:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "http://api.openweathermap.org/data/2.5/weather"
def get_weather(self, city):
"""Get current weather for a city"""
params = {
'q': city,
'appid': self.api_key,
'units': 'metric'
}
try:
response = requests.get(self.base_url, params=params)
response.raise_for_status() # Raise exception for bad status codes
data = response.json()
return self.format_weather(data)
except requests.exceptions.RequestException as e:
return f"Error fetching weather: {e}"
except KeyError as e:
return f"Unexpected response format: {e}"
def format_weather(self, data):
"""Format weather data into readable string"""
city = data['name']
country = data['sys']['country']
temp = data['main']['temp']
feels_like = data['main']['feels_like']
description = data['weather'][0]['description']
humidity = data['main']['humidity']
return f"""
Weather in {city}, {country}:
Temperature: {temp}°C (feels like {feels_like}°C)
Condition: {description.title()}
Humidity: {humidity}%
""".strip()
# Usage
def main():
api_key = "your_api_key_here" # Get from openweathermap.org
weather = WeatherChecker(api_key)
while True:
city = input("\nEnter city name (or 'quit' to exit): ")
if city.lower() == 'quit':
break
result = weather.get_weather(city)
print(result)
if __name__ == "__main__":
main()
Data Processing and Analysis
Python shines at processing data. Here’s a log analyzer that finds patterns in web server logs:
import re
from collections import Counter, defaultdict
from datetime import datetime
class LogAnalyzer:
def __init__(self, log_file):
self.log_file = log_file
self.log_pattern = re.compile(
r'(\d+\.\d+\.\d+\.\d+) - - \[(.*?)\] "(.*?)" (\d+) (\d+)'
)
def parse_logs(self):
"""Parse log file and extract useful information"""
logs = []
try:
with open(self.log_file, 'r') as file:
for line in file:
match = self.log_pattern.match(line.strip())
if match:
ip, timestamp, request, status, size = match.groups()
# Parse request to get method and path
request_parts = request.split()
method = request_parts[0] if request_parts else 'UNKNOWN'
path = request_parts[1] if len(request_parts) > 1 else '/'
logs.append({
'ip': ip,
'timestamp': timestamp,
'method': method,
'path': path,
'status': int(status),
'size': int(size) if size.isdigit() else 0
})
except FileNotFoundError:
print(f"Log file {self.log_file} not found")
return []
return logs
def analyze(self):
"""Analyze logs and generate report"""
logs = self.parse_logs()
if not logs:
return
print(f"Analyzed {len(logs)} log entries\n")
# Top IP addresses
ip_counter = Counter(log['ip'] for log in logs)
print("Top 5 IP addresses:")
for ip, count in ip_counter.most_common(5):
print(f" {ip}: {count} requests")
# Status code distribution
status_counter = Counter(log['status'] for log in logs)
print("\nStatus code distribution:")
for status, count in sorted(status_counter.items()):
print(f" {status}: {count} requests")
# Most requested paths
path_counter = Counter(log['path'] for log in logs)
print("\nTop 5 requested paths:")
for path, count in path_counter.most_common(5):
print(f" {path}: {count} requests")
# Error analysis
errors = [log for log in logs if log['status'] >= 400]
if errors:
print(f"\nFound {len(errors)} error responses")
error_ips = Counter(log['ip'] for log in errors)
print("Top error-generating IPs:")
for ip, count in error_ips.most_common(3):
print(f" {ip}: {count} errors")
# Usage
if __name__ == "__main__":
analyzer = LogAnalyzer("access.log")
analyzer.analyze()
Automation Scripts
Python is perfect for automating repetitive tasks. Here’s a backup script:
import os
import shutil
import zipfile
from datetime import datetime
from pathlib import Path
class BackupManager:
def __init__(self, source_dir, backup_dir):
self.source_dir = Path(source_dir)
self.backup_dir = Path(backup_dir)
self.backup_dir.mkdir(exist_ok=True)
def create_backup(self, compress=True):
"""Create a backup of the source directory"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if compress:
backup_name = f"backup_{timestamp}.zip"
backup_path = self.backup_dir / backup_name
self.create_zip_backup(backup_path)
else:
backup_name = f"backup_{timestamp}"
backup_path = self.backup_dir / backup_name
self.create_folder_backup(backup_path)
print(f"Backup created: {backup_path}")
return backup_path
def create_zip_backup(self, backup_path):
"""Create compressed backup"""
with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in self.source_dir.rglob('*'):
if file_path.is_file():
# Calculate relative path for zip
relative_path = file_path.relative_to(self.source_dir)
zipf.write(file_path, relative_path)
def create_folder_backup(self, backup_path):
"""Create uncompressed backup"""
shutil.copytree(self.source_dir, backup_path)
def cleanup_old_backups(self, keep_count=5):
"""Keep only the most recent backups"""
backups = []
for item in self.backup_dir.iterdir():
if item.name.startswith('backup_'):
backups.append(item)
# Sort by modification time (newest first)
backups.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# Remove old backups
for old_backup in backups[keep_count:]:
if old_backup.is_file():
old_backup.unlink()
else:
shutil.rmtree(old_backup)
print(f"Removed old backup: {old_backup.name}")
# Usage
def main():
source = input("Enter source directory path: ")
backup_location = input("Enter backup directory path: ")
backup_manager = BackupManager(source, backup_location)
# Create backup
backup_manager.create_backup(compress=True)
# Clean up old backups
backup_manager.cleanup_old_backups(keep_count=3)
if __name__ == "__main__":
main()
Text Processing and Analysis
Python excels at text processing. Here’s a simple text analyzer:
import string
from collections import Counter
class TextAnalyzer:
def __init__(self, text):
self.text = text
self.words = self.extract_words()
def extract_words(self):
"""Extract words from text, removing punctuation"""
# Remove punctuation and convert to lowercase
translator = str.maketrans('', '', string.punctuation)
clean_text = self.text.translate(translator).lower()
# Split into words and filter empty strings
words = [word for word in clean_text.split() if word]
return words
def word_count(self):
"""Count total words"""
return len(self.words)
def unique_words(self):
"""Count unique words"""
return len(set(self.words))
def most_common_words(self, n=10):
"""Find most common words"""
counter = Counter(self.words)
return counter.most_common(n)
def average_word_length(self):
"""Calculate average word length"""
if not self.words:
return 0
total_length = sum(len(word) for word in self.words)
return total_length / len(self.words)
def reading_time(self, wpm=200):
"""Estimate reading time in minutes"""
return self.word_count() / wpm
def generate_report(self):
"""Generate comprehensive text analysis report"""
report = f"""
Text Analysis Report
{'=' * 20}
Total words: {self.word_count()}
Unique words: {self.unique_words()}
Average word length: {self.average_word_length():.1f} characters
Estimated reading time: {self.reading_time():.1f} minutes
Most common words:
"""
for word, count in self.most_common_words(5):
report += f" {word}: {count}\n"
return report
# Usage
def analyze_file(filename):
try:
with open(filename, 'r', encoding='utf-8') as file:
text = file.read()
analyzer = TextAnalyzer(text)
print(analyzer.generate_report())
except FileNotFoundError:
print(f"File {filename} not found")
if __name__ == "__main__":
filename = input("Enter text file path: ")
analyze_file(filename)
Simple Web Server
Python can even create web servers with just a few lines:
from http.server import HTTPServer, SimpleHTTPRequestHandler
import json
from urllib.parse import urlparse, parse_qs
class CustomHandler(SimpleHTTPRequestHandler):
def do_GET(self):
"""Handle GET requests"""
parsed_path = urlparse(self.path)
if parsed_path.path == '/api/hello':
self.send_json_response({'message': 'Hello from Python!'})
elif parsed_path.path == '/api/time':
from datetime import datetime
current_time = datetime.now().isoformat()
self.send_json_response({'time': current_time})
else:
# Serve static files
super().do_GET()
def send_json_response(self, data):
"""Send JSON response"""
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
json_data = json.dumps(data)
self.wfile.write(json_data.encode())
def run_server(port=8000):
"""Run the web server"""
server_address = ('', port)
httpd = HTTPServer(server_address, CustomHandler)
print(f"Server running on http://localhost:{port}")
print("Press Ctrl+C to stop")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
httpd.server_close()
if __name__ == "__main__":
run_server()
These examples demonstrate how Python’s fundamentals combine to create useful applications. Each script uses the core concepts we covered - variables, functions, loops, error handling, and data structures - to solve real problems.
The key insight: start with a problem you want to solve, then use Python’s tools to build a solution. Don’t worry about writing perfect code initially - focus on making it work, then improve it.
Next, we’ll explore advanced techniques and patterns that will help you write more efficient, maintainable Python code.
Advanced Techniques and Patterns
The moment I discovered list comprehensions, my Python code became dramatically more concise and readable. What used to take 5 lines could now be done in 1. But list comprehensions were just the beginning - Python has many elegant features that separate beginner code from professional-quality code.
These advanced techniques aren’t just about showing off - they make your code more readable, efficient, and maintainable. Let’s explore the patterns that will elevate your Python skills.
List Comprehensions and Beyond
List comprehensions are Python’s way of creating lists concisely. Once you master them, you’ll use them everywhere:
# Basic list comprehension
squares = [x**2 for x in range(10)]
# With conditions
even_squares = [x**2 for x in range(10) if x % 2 == 0]
# Processing existing data
names = ['alice', 'bob', 'charlie']
capitalized = [name.capitalize() for name in names]
# Nested comprehensions (use sparingly)
matrix = [[i*j for j in range(3)] for i in range(3)]
# Dictionary comprehensions
word_lengths = {word: len(word) for word in names}
# Set comprehensions
unique_lengths = {len(word) for word in names}
Generators - Memory-Efficient Iteration
Generators are one of Python’s most powerful features. They create iterators that generate values on-demand, saving memory:
# Generator function
def fibonacci_generator(n):
"""Generate fibonacci numbers up to n"""
a, b = 0, 1
while a < n:
yield a
a, b = b, a + b
# Generator expression
squares_gen = (x**2 for x in range(1000000)) # Uses minimal memory
# Using generators
for fib in fibonacci_generator(100):
print(fib)
# Generators are memory efficient
import sys
list_comp = [x**2 for x in range(1000)] # Creates full list in memory
gen_exp = (x**2 for x in range(1000)) # Creates generator object
print(f"List size: {sys.getsizeof(list_comp)} bytes")
print(f"Generator size: {sys.getsizeof(gen_exp)} bytes")
Decorators - Enhancing Functions
Decorators let you modify or enhance functions without changing their code:
import time
from functools import wraps
def timing_decorator(func):
"""Decorator to measure function execution time"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds")
return result
return wrapper
def retry_decorator(max_attempts=3):
"""Decorator to retry function on failure"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
print(f"Attempt {attempt + 1} failed: {e}")
return None
return wrapper
return decorator
# Using decorators
@timing_decorator
@retry_decorator(max_attempts=3)
def unreliable_function():
"""Function that sometimes fails"""
import random
if random.random() < 0.7:
raise Exception("Random failure")
return "Success!"
Context Managers - Resource Management
Context managers ensure proper resource cleanup using the with
statement:
# Built-in context managers
with open('file.txt', 'r') as file:
content = file.read()
# File automatically closed
# Custom context manager using class
class DatabaseConnection:
def __init__(self, db_name):
self.db_name = db_name
self.connection = None
def __enter__(self):
print(f"Connecting to {self.db_name}")
self.connection = f"Connection to {self.db_name}"
return self.connection
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Closing connection to {self.db_name}")
self.connection = None
# Custom context manager using contextlib
from contextlib import contextmanager
@contextmanager
def temporary_file(filename):
"""Context manager for temporary files"""
import os
try:
# Setup
with open(filename, 'w') as f:
f.write("Temporary content")
yield filename
finally:
# Cleanup
if os.path.exists(filename):
os.remove(filename)
# Using context managers
with DatabaseConnection("mydb") as conn:
print(f"Using {conn}")
with temporary_file("temp.txt") as temp_file:
print(f"Working with {temp_file}")
Advanced Function Techniques
Python functions have many advanced features:
# Default arguments and keyword arguments
def create_user(name, email, age=None, **kwargs):
"""Create user with flexible arguments"""
user = {
'name': name,
'email': email,
'age': age
}
user.update(kwargs) # Add any additional keyword arguments
return user
# Variable arguments
def calculate_average(*numbers):
"""Calculate average of any number of arguments"""
if not numbers:
return 0
return sum(numbers) / len(numbers)
# Lambda functions (anonymous functions)
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Higher-order functions
def apply_operation(numbers, operation):
"""Apply operation to all numbers"""
return [operation(x) for x in numbers]
def double(x):
return x * 2
def square(x):
return x ** 2
# Usage
user = create_user("Alice", "[email protected]", role="admin", active=True)
avg = calculate_average(1, 2, 3, 4, 5)
doubled = apply_operation([1, 2, 3], double)
Object-Oriented Programming Basics
Classes help organize code and model real-world concepts:
class BankAccount:
"""Simple bank account class"""
def __init__(self, account_number, initial_balance=0):
self.account_number = account_number
self._balance = initial_balance # Protected attribute
self._transactions = []
@property
def balance(self):
"""Get current balance"""
return self._balance
def deposit(self, amount):
"""Deposit money"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self._balance += amount
self._transactions.append(f"Deposit: +${amount}")
return self._balance
def withdraw(self, amount):
"""Withdraw money"""
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
self._transactions.append(f"Withdrawal: -${amount}")
return self._balance
def get_statement(self):
"""Get account statement"""
statement = f"Account: {self.account_number}\n"
statement += f"Balance: ${self._balance}\n"
statement += "Recent transactions:\n"
for transaction in self._transactions[-5:]: # Last 5 transactions
statement += f" {transaction}\n"
return statement
def __str__(self):
return f"BankAccount({self.account_number}, ${self._balance})"
# Usage
account = BankAccount("12345", 1000)
account.deposit(500)
account.withdraw(200)
print(account.get_statement())
Error Handling Patterns
Proper error handling makes your code robust:
class ValidationError(Exception):
"""Custom exception for validation errors"""
pass
def validate_email(email):
"""Validate email format"""
if '@' not in email:
raise ValidationError("Email must contain @ symbol")
if '.' not in email.split('@')[1]:
raise ValidationError("Email domain must contain a dot")
return True
def safe_divide(a, b):
"""Safely divide two numbers"""
try:
result = a / b
return result, None
except ZeroDivisionError:
return None, "Cannot divide by zero"
except TypeError:
return None, "Arguments must be numbers"
def process_user_data(data):
"""Process user data with comprehensive error handling"""
try:
# Validate required fields
if 'email' not in data:
raise ValidationError("Email is required")
validate_email(data['email'])
# Process data
processed = {
'email': data['email'].lower().strip(),
'name': data.get('name', '').strip(),
'age': int(data.get('age', 0))
}
return processed, None
except ValidationError as e:
return None, f"Validation error: {e}"
except ValueError as e:
return None, f"Value error: {e}"
except Exception as e:
return None, f"Unexpected error: {e}"
# Usage with error handling
user_data = {'email': '[email protected]', 'name': 'Alice', 'age': '30'}
result, error = process_user_data(user_data)
if error:
print(f"Error: {error}")
else:
print(f"Processed: {result}")
Working with Files and Data
Advanced file handling patterns:
import json
import csv
from pathlib import Path
class DataManager:
"""Manage different data formats"""
def __init__(self, data_dir):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(exist_ok=True)
def save_json(self, filename, data):
"""Save data as JSON"""
file_path = self.data_dir / f"{filename}.json"
with open(file_path, 'w') as f:
json.dump(data, f, indent=2)
def load_json(self, filename):
"""Load data from JSON"""
file_path = self.data_dir / f"{filename}.json"
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
return None
def save_csv(self, filename, data, headers):
"""Save data as CSV"""
file_path = self.data_dir / f"{filename}.csv"
with open(file_path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(data)
def load_csv(self, filename):
"""Load data from CSV"""
file_path = self.data_dir / f"{filename}.csv"
try:
with open(file_path, 'r') as f:
return list(csv.DictReader(f))
except FileNotFoundError:
return []
# Usage
dm = DataManager("data")
# Save and load JSON
user_data = {'name': 'Alice', 'age': 30, 'email': '[email protected]'}
dm.save_json('user', user_data)
loaded_user = dm.load_json('user')
# Save and load CSV
users = [
{'name': 'Alice', 'age': 30, 'email': '[email protected]'},
{'name': 'Bob', 'age': 25, 'email': '[email protected]'}
]
dm.save_csv('users', users, ['name', 'age', 'email'])
loaded_users = dm.load_csv('users')
Functional Programming Concepts
Python supports functional programming patterns:
from functools import reduce, partial
# Map, filter, reduce
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Map - transform each element
squared = list(map(lambda x: x**2, numbers))
# Filter - select elements that meet criteria
evens = list(filter(lambda x: x % 2 == 0, numbers))
# Reduce - combine elements into single value
sum_all = reduce(lambda x, y: x + y, numbers)
product = reduce(lambda x, y: x * y, numbers)
# Partial functions
def multiply(x, y):
return x * y
double = partial(multiply, 2) # Fix first argument to 2
triple = partial(multiply, 3) # Fix first argument to 3
# Usage
print(double(5)) # 10
print(triple(4)) # 12
# Function composition
def compose(f, g):
"""Compose two functions"""
return lambda x: f(g(x))
def add_one(x):
return x + 1
def square(x):
return x ** 2
# Compose functions
add_then_square = compose(square, add_one)
print(add_then_square(3)) # (3 + 1)^2 = 16
These advanced techniques transform how you write Python code. They’re not just syntactic sugar - they represent different ways of thinking about problems and solutions. Master these patterns, and your code will become more elegant, efficient, and maintainable.
Next, we’ll explore best practices and optimization techniques that ensure your Python code is not just functional, but professional-quality.
Best Practices and Optimization
The difference between code that works and code that works well lies in the details. I learned this the hard way when my first Python script took 10 minutes to process a file that should have taken 10 seconds. The problem wasn’t the algorithm - it was dozens of small inefficiencies that added up to a performance disaster.
Writing good Python code isn’t just about making it work; it’s about making it readable, maintainable, and efficient. These best practices will help you write code that other developers (including future you) will thank you for.
Code Style and PEP 8
Python has official style guidelines called PEP 8. Following them makes your code more readable and professional:
# Good: Clear, readable code following PEP 8
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly mortgage payment."""
monthly_rate = annual_rate / 12
num_payments = years * 12
if monthly_rate == 0:
return principal / num_payments
payment = principal * (monthly_rate * (1 + monthly_rate) ** num_payments) / \
((1 + monthly_rate) ** num_payments - 1)
return round(payment, 2)
# Bad: Hard to read, doesn't follow conventions
def calc(p,r,y):
mr=r/12
n=y*12
if mr==0:return p/n
pmt=p*(mr*(1+mr)**n)/((1+mr)**n-1)
return round(pmt,2)
Key PEP 8 guidelines:
- Use 4 spaces for indentation (not tabs)
- Keep lines under 79 characters
- Use descriptive variable names
- Add spaces around operators
- Use lowercase with underscores for function names
- Use docstrings to document functions
Error Handling Best Practices
Good error handling makes your code robust and user-friendly:
import logging
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class FileProcessor:
"""Process files with proper error handling"""
def __init__(self, input_dir, output_dir):
self.input_dir = Path(input_dir)
self.output_dir = Path(output_dir)
# Create output directory if it doesn't exist
self.output_dir.mkdir(parents=True, exist_ok=True)
def process_file(self, filename):
"""Process a single file with comprehensive error handling"""
input_path = self.input_dir / filename
output_path = self.output_dir / f"processed_{filename}"
try:
# Validate input file exists
if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
# Process file
with open(input_path, 'r', encoding='utf-8') as infile:
content = infile.read()
# Transform content (example: uppercase)
processed_content = content.upper()
# Write output
with open(output_path, 'w', encoding='utf-8') as outfile:
outfile.write(processed_content)
logger.info(f"Successfully processed {filename}")
return True
except FileNotFoundError as e:
logger.error(f"File error: {e}")
return False
except PermissionError as e:
logger.error(f"Permission error: {e}")
return False
except UnicodeDecodeError as e:
logger.error(f"Encoding error in {filename}: {e}")
return False
except Exception as e:
logger.error(f"Unexpected error processing {filename}: {e}")
return False
def process_all_files(self, pattern="*.txt"):
"""Process all files matching pattern"""
processed_count = 0
error_count = 0
for file_path in self.input_dir.glob(pattern):
if self.process_file(file_path.name):
processed_count += 1
else:
error_count += 1
logger.info(f"Processing complete: {processed_count} successful, {error_count} errors")
return processed_count, error_count
Performance Optimization
Small optimizations can make big differences in Python performance:
import time
from collections import defaultdict, Counter
def timing_comparison():
"""Compare different approaches for common operations"""
# String concatenation
def slow_string_concat(items):
result = ""
for item in items:
result += str(item)
return result
def fast_string_concat(items):
return "".join(str(item) for item in items)
# List operations
def slow_list_search(items, target):
for item in items:
if item == target:
return True
return False
def fast_list_search(items, target):
return target in set(items) # Convert to set for O(1) lookup
# Dictionary operations
def slow_counting(items):
counts = {}
for item in items:
if item in counts:
counts[item] += 1
else:
counts[item] = 1
return counts
def fast_counting(items):
return Counter(items)
# Test data
test_items = list(range(10000))
# Time string concatenation
start = time.time()
slow_result = slow_string_concat(test_items[:1000])
slow_time = time.time() - start
start = time.time()
fast_result = fast_string_concat(test_items[:1000])
fast_time = time.time() - start
print(f"String concatenation - Slow: {slow_time:.4f}s, Fast: {fast_time:.4f}s")
print(f"Speedup: {slow_time/fast_time:.1f}x")
# Memory-efficient generators
def memory_efficient_processing():
"""Demonstrate memory-efficient patterns"""
# Bad: Loads entire file into memory
def process_large_file_bad(filename):
with open(filename, 'r') as f:
lines = f.readlines() # Loads everything into memory
processed = []
for line in lines:
processed.append(line.strip().upper())
return processed
# Good: Process line by line
def process_large_file_good(filename):
with open(filename, 'r') as f:
for line in f: # Generator - one line at a time
yield line.strip().upper()
# Usage
# for processed_line in process_large_file_good('large_file.txt'):
# print(processed_line)
# Efficient data structures
class OptimizedDataProcessor:
"""Use appropriate data structures for better performance"""
def __init__(self):
self.lookup_data = set() # O(1) lookups
self.counter_data = Counter() # Efficient counting
self.grouped_data = defaultdict(list) # Automatic list creation
def add_item(self, item, category):
"""Add item with category"""
self.lookup_data.add(item)
self.counter_data[item] += 1
self.grouped_data[category].append(item)
def is_item_present(self, item):
"""Fast lookup - O(1) instead of O(n)"""
return item in self.lookup_data
def get_most_common(self, n=5):
"""Get most common items efficiently"""
return self.counter_data.most_common(n)
Code Organization and Structure
Well-organized code is easier to maintain and debug:
# config.py - Configuration management
import os
from pathlib import Path
class Config:
"""Application configuration"""
# Directories
BASE_DIR = Path(__file__).parent
DATA_DIR = BASE_DIR / "data"
LOG_DIR = BASE_DIR / "logs"
# Database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///app.db")
# API settings
API_KEY = os.getenv("API_KEY")
API_TIMEOUT = int(os.getenv("API_TIMEOUT", "30"))
# Logging
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
@classmethod
def validate(cls):
"""Validate configuration"""
if not cls.API_KEY:
raise ValueError("API_KEY environment variable is required")
# Create directories
cls.DATA_DIR.mkdir(exist_ok=True)
cls.LOG_DIR.mkdir(exist_ok=True)
# utils.py - Utility functions
import functools
import time
def retry(max_attempts=3, delay=1):
"""Decorator to retry functions on failure"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise e
time.sleep(delay)
return None
return wrapper
return decorator
def validate_input(validator_func):
"""Decorator to validate function inputs"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not validator_func(*args, **kwargs):
raise ValueError("Input validation failed")
return func(*args, **kwargs)
return wrapper
return decorator
# main.py - Main application
import logging
from config import Config
from utils import retry, validate_input
def setup_logging():
"""Set up application logging"""
logging.basicConfig(
level=getattr(logging, Config.LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(Config.LOG_DIR / 'app.log'),
logging.StreamHandler()
]
)
class Application:
"""Main application class"""
def __init__(self):
Config.validate()
setup_logging()
self.logger = logging.getLogger(__name__)
@retry(max_attempts=3)
def fetch_data(self, url):
"""Fetch data with retry logic"""
import requests
response = requests.get(url, timeout=Config.API_TIMEOUT)
response.raise_for_status()
return response.json()
def run(self):
"""Run the application"""
self.logger.info("Application starting")
try:
# Application logic here
pass
except Exception as e:
self.logger.error(f"Application error: {e}")
raise
finally:
self.logger.info("Application finished")
if __name__ == "__main__":
app = Application()
app.run()
Testing and Documentation
Good code includes tests and documentation:
def calculate_compound_interest(principal, rate, time, compound_frequency=1):
"""
Calculate compound interest.
Args:
principal (float): Initial amount of money
rate (float): Annual interest rate (as decimal, e.g., 0.05 for 5%)
time (float): Time period in years
compound_frequency (int): Number of times interest compounds per year
Returns:
float: Final amount after compound interest
Raises:
ValueError: If any parameter is negative
Examples:
>>> calculate_compound_interest(1000, 0.05, 2)
1102.5
>>> calculate_compound_interest(1000, 0.05, 2, 12)
1104.89
"""
if principal < 0 or rate < 0 or time < 0 or compound_frequency <= 0:
raise ValueError("All parameters must be non-negative (compound_frequency must be positive)")
amount = principal * (1 + rate / compound_frequency) ** (compound_frequency * time)
return round(amount, 2)
# Simple test function
def test_compound_interest():
"""Test compound interest calculation"""
# Test basic calculation
result = calculate_compound_interest(1000, 0.05, 2)
assert abs(result - 1102.5) < 0.01, f"Expected ~1102.5, got {result}"
# Test with monthly compounding
result = calculate_compound_interest(1000, 0.05, 2, 12)
assert abs(result - 1104.89) < 0.01, f"Expected ~1104.89, got {result}"
# Test error handling
try:
calculate_compound_interest(-1000, 0.05, 2)
assert False, "Should have raised ValueError"
except ValueError:
pass # Expected
print("All tests passed!")
if __name__ == "__main__":
test_compound_interest()
Security Best Practices
Security should be built into your code from the start:
import hashlib
import secrets
import os
from pathlib import Path
class SecureFileHandler:
"""Handle files securely"""
def __init__(self, allowed_dir):
self.allowed_dir = Path(allowed_dir).resolve()
def safe_file_path(self, filename):
"""Ensure file path is within allowed directory"""
# Remove any path traversal attempts
safe_name = os.path.basename(filename)
full_path = (self.allowed_dir / safe_name).resolve()
# Ensure path is within allowed directory
if not str(full_path).startswith(str(self.allowed_dir)):
raise ValueError("Invalid file path")
return full_path
def hash_password(self, password):
"""Hash password securely"""
# Generate random salt
salt = secrets.token_hex(32)
# Hash password with salt
password_hash = hashlib.pbkdf2_hmac('sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000) # 100,000 iterations
return salt + password_hash.hex()
def verify_password(self, password, stored_hash):
"""Verify password against stored hash"""
# Extract salt (first 64 characters)
salt = stored_hash[:64]
stored_password_hash = stored_hash[64:]
# Hash provided password with same salt
password_hash = hashlib.pbkdf2_hmac('sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000)
return password_hash.hex() == stored_password_hash
# Input validation
def validate_email(email):
"""Basic email validation"""
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
def sanitize_input(user_input, max_length=100):
"""Sanitize user input"""
if not isinstance(user_input, str):
raise ValueError("Input must be a string")
# Remove potentially dangerous characters
sanitized = re.sub(r'[<>"\']', '', user_input)
# Limit length
sanitized = sanitized[:max_length]
# Strip whitespace
return sanitized.strip()
These best practices transform good code into great code. They’re not just academic exercises - they solve real problems that emerge when code moves from development to production. Follow these patterns, and your Python code will be more reliable, maintainable, and professional.
Next, we’ll put everything together with real-world projects that demonstrate how these fundamentals, techniques, and best practices combine to create complete applications.
Real-World Projects and Implementation
The best way to solidify your Python knowledge is by building complete projects that solve real problems. I remember my first substantial Python project - a personal expense tracker that helped me understand where my money was going. It wasn’t perfect, but it worked, and more importantly, it taught me how all the Python concepts fit together.
Let’s build three complete projects that demonstrate different aspects of Python programming: a task management system, a web scraper with data analysis, and a simple web application.
Project 1: Personal Task Manager
This project combines file handling, object-oriented programming, and command-line interfaces:
# task_manager.py
import json
import datetime
from pathlib import Path
from enum import Enum
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
class Task:
"""Represents a single task"""
def __init__(self, title, description="", priority=Priority.MEDIUM, due_date=None):
self.id = None # Will be set by TaskManager
self.title = title
self.description = description
self.priority = priority
self.due_date = due_date
self.completed = False
self.created_at = datetime.datetime.now()
self.completed_at = None
def mark_completed(self):
"""Mark task as completed"""
self.completed = True
self.completed_at = datetime.datetime.now()
def to_dict(self):
"""Convert task to dictionary for JSON serialization"""
return {
'id': self.id,
'title': self.title,
'description': self.description,
'priority': self.priority.value,
'due_date': self.due_date.isoformat() if self.due_date else None,
'completed': self.completed,
'created_at': self.created_at.isoformat(),
'completed_at': self.completed_at.isoformat() if self.completed_at else None
}
@classmethod
def from_dict(cls, data):
"""Create task from dictionary"""
task = cls(
title=data['title'],
description=data['description'],
priority=Priority(data['priority'])
)
task.id = data['id']
task.completed = data['completed']
task.created_at = datetime.datetime.fromisoformat(data['created_at'])
if data['due_date']:
task.due_date = datetime.datetime.fromisoformat(data['due_date'])
if data['completed_at']:
task.completed_at = datetime.datetime.fromisoformat(data['completed_at'])
return task
class TaskManager:
"""Manages a collection of tasks"""
def __init__(self, data_file="tasks.json"):
self.data_file = Path(data_file)
self.tasks = []
self.next_id = 1
self.load_tasks()
def add_task(self, title, description="", priority=Priority.MEDIUM, due_date=None):
"""Add a new task"""
task = Task(title, description, priority, due_date)
task.id = self.next_id
self.next_id += 1
self.tasks.append(task)
self.save_tasks()
return task
def complete_task(self, task_id):
"""Mark a task as completed"""
task = self.get_task(task_id)
if task:
task.mark_completed()
self.save_tasks()
return True
return False
def delete_task(self, task_id):
"""Delete a task"""
task = self.get_task(task_id)
if task:
self.tasks.remove(task)
self.save_tasks()
return True
return False
def get_task(self, task_id):
"""Get task by ID"""
for task in self.tasks:
if task.id == task_id:
return task
return None
def list_tasks(self, show_completed=False, priority_filter=None):
"""List tasks with optional filters"""
filtered_tasks = []
for task in self.tasks:
# Filter by completion status
if not show_completed and task.completed:
continue
# Filter by priority
if priority_filter and task.priority != priority_filter:
continue
filtered_tasks.append(task)
# Sort by priority (high to low) then by due date
filtered_tasks.sort(key=lambda t: (
-t.priority.value,
t.due_date or datetime.datetime.max
))
return filtered_tasks
def save_tasks(self):
"""Save tasks to JSON file"""
data = {
'next_id': self.next_id,
'tasks': [task.to_dict() for task in self.tasks]
}
with open(self.data_file, 'w') as f:
json.dump(data, f, indent=2)
def load_tasks(self):
"""Load tasks from JSON file"""
if not self.data_file.exists():
return
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
self.next_id = data.get('next_id', 1)
self.tasks = [Task.from_dict(task_data) for task_data in data.get('tasks', [])]
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading tasks: {e}")
def main():
"""Command-line interface for task manager"""
manager = TaskManager()
while True:
print("\n=== Personal Task Manager ===")
print("1. Add task")
print("2. List tasks")
print("3. Complete task")
print("4. Delete task")
print("5. Quit")
choice = input("\nEnter your choice (1-5): ").strip()
if choice == '1':
title = input("Task title: ").strip()
description = input("Description (optional): ").strip()
# Priority selection
print("Priority: 1=Low, 2=Medium, 3=High")
priority_input = input("Priority (default=2): ").strip()
try:
priority = Priority(int(priority_input) if priority_input else 2)
except ValueError:
priority = Priority.MEDIUM
# Due date
due_date_input = input("Due date (YYYY-MM-DD, optional): ").strip()
due_date = None
if due_date_input:
try:
due_date = datetime.datetime.strptime(due_date_input, "%Y-%m-%d")
except ValueError:
print("Invalid date format, skipping due date")
task = manager.add_task(title, description, priority, due_date)
print(f"Added task: {task.title}")
elif choice == '2':
show_completed = input("Show completed tasks? (y/n): ").lower() == 'y'
tasks = manager.list_tasks(show_completed=show_completed)
if not tasks:
print("No tasks found.")
else:
print(f"\n{'ID':<4} {'Title':<30} {'Priority':<8} {'Due Date':<12} {'Status'}")
print("-" * 70)
for task in tasks:
due_str = task.due_date.strftime("%Y-%m-%d") if task.due_date else "None"
status = "✓ Done" if task.completed else "Pending"
priority_str = task.priority.name
print(f"{task.id:<4} {task.title[:29]:<30} {priority_str:<8} {due_str:<12} {status}")
elif choice == '3':
try:
task_id = int(input("Enter task ID to complete: "))
if manager.complete_task(task_id):
print("Task marked as completed!")
else:
print("Task not found.")
except ValueError:
print("Invalid task ID.")
elif choice == '4':
try:
task_id = int(input("Enter task ID to delete: "))
if manager.delete_task(task_id):
print("Task deleted!")
else:
print("Task not found.")
except ValueError:
print("Invalid task ID.")
elif choice == '5':
print("Goodbye!")
break
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main()
Project 2: Web Scraper with Data Analysis
This project demonstrates web scraping, data processing, and basic analysis:
# news_analyzer.py
import requests
from bs4 import BeautifulSoup
import pandas as pd
from collections import Counter
import matplotlib.pyplot as plt
from wordcloud import WordCloud
import re
class NewsAnalyzer:
"""Scrape and analyze news articles"""
def __init__(self):
self.articles = []
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
def scrape_hacker_news(self, num_pages=3):
"""Scrape articles from Hacker News"""
base_url = "https://news.ycombinator.com"
for page in range(num_pages):
url = f"{base_url}/?p={page + 1}" if page > 0 else base_url
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# Find article titles and links
title_links = soup.find_all('a', class_='storylink')
for link in title_links:
title = link.get_text().strip()
url = link.get('href', '')
# Get score and comments
score_elem = link.find_parent('tr').find_next_sibling('tr')
if score_elem:
score_text = score_elem.find('span', class_='score')
score = int(re.findall(r'\d+', score_text.get_text())[0]) if score_text else 0
comments_link = score_elem.find('a', string=re.compile(r'\d+\s+comment'))
comments = int(re.findall(r'\d+', comments_link.get_text())[0]) if comments_link else 0
else:
score = 0
comments = 0
self.articles.append({
'title': title,
'url': url,
'score': score,
'comments': comments,
'source': 'Hacker News'
})
print(f"Scraped page {page + 1}")
except requests.RequestException as e:
print(f"Error scraping page {page + 1}: {e}")
def analyze_data(self):
"""Analyze scraped articles"""
if not self.articles:
print("No articles to analyze")
return
df = pd.DataFrame(self.articles)
print(f"\n=== Analysis of {len(df)} articles ===")
# Basic statistics
print(f"Average score: {df['score'].mean():.1f}")
print(f"Average comments: {df['comments'].mean():.1f}")
print(f"Most popular article: {df.loc[df['score'].idxmax(), 'title']}")
# Word frequency analysis
all_titles = ' '.join(df['title'].tolist())
words = re.findall(r'\b[a-zA-Z]{3,}\b', all_titles.lower())
# Remove common words
stop_words = {'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'man', 'new', 'now', 'old', 'see', 'two', 'way', 'who', 'boy', 'did', 'its', 'let', 'put', 'say', 'she', 'too', 'use'}
filtered_words = [word for word in words if word not in stop_words]
word_freq = Counter(filtered_words)
print(f"\nTop 10 words:")
for word, count in word_freq.most_common(10):
print(f" {word}: {count}")
# Create visualizations
self.create_visualizations(df, word_freq)
return df
def create_visualizations(self, df, word_freq):
"""Create charts and visualizations"""
# Score distribution
plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
plt.hist(df['score'], bins=20, alpha=0.7, color='skyblue')
plt.title('Score Distribution')
plt.xlabel('Score')
plt.ylabel('Frequency')
# Comments vs Score scatter plot
plt.subplot(2, 2, 2)
plt.scatter(df['score'], df['comments'], alpha=0.6)
plt.title('Comments vs Score')
plt.xlabel('Score')
plt.ylabel('Comments')
# Top words bar chart
plt.subplot(2, 2, 3)
top_words = dict(word_freq.most_common(10))
plt.bar(top_words.keys(), top_words.values())
plt.title('Top 10 Words')
plt.xticks(rotation=45)
plt.ylabel('Frequency')
# Word cloud
plt.subplot(2, 2, 4)
if word_freq:
wordcloud = WordCloud(width=400, height=300, background_color='white').generate_from_frequencies(word_freq)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.title('Word Cloud')
plt.tight_layout()
plt.savefig('news_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
def save_data(self, filename='articles.csv'):
"""Save articles to CSV file"""
if self.articles:
df = pd.DataFrame(self.articles)
df.to_csv(filename, index=False)
print(f"Data saved to {filename}")
def main():
analyzer = NewsAnalyzer()
print("Scraping Hacker News articles...")
analyzer.scrape_hacker_news(num_pages=2)
print("Analyzing data...")
df = analyzer.analyze_data()
analyzer.save_data()
if __name__ == "__main__":
main()
Project 3: Simple Web Application
This project creates a web application using Flask:
# app.py
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
import sqlite3
import datetime
from contextlib import contextmanager
app = Flask(__name__)
app.secret_key = 'your-secret-key-here'
class DatabaseManager:
"""Handle database operations"""
def __init__(self, db_path='expenses.db'):
self.db_path = db_path
self.init_database()
@contextmanager
def get_connection(self):
"""Context manager for database connections"""
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row # Enable column access by name
try:
yield conn
finally:
conn.close()
def init_database(self):
"""Initialize database tables"""
with self.get_connection() as conn:
conn.execute('''
CREATE TABLE IF NOT EXISTS expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
description TEXT NOT NULL,
amount REAL NOT NULL,
category TEXT NOT NULL,
date TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
def add_expense(self, description, amount, category, date):
"""Add new expense"""
with self.get_connection() as conn:
conn.execute(
'INSERT INTO expenses (description, amount, category, date) VALUES (?, ?, ?, ?)',
(description, amount, category, date)
)
conn.commit()
def get_expenses(self, limit=None):
"""Get all expenses"""
with self.get_connection() as conn:
query = 'SELECT * FROM expenses ORDER BY date DESC'
if limit:
query += f' LIMIT {limit}'
cursor = conn.execute(query)
return cursor.fetchall()
def get_expense_summary(self):
"""Get expense summary by category"""
with self.get_connection() as conn:
cursor = conn.execute('''
SELECT category, SUM(amount) as total, COUNT(*) as count
FROM expenses
GROUP BY category
ORDER BY total DESC
''')
return cursor.fetchall()
def delete_expense(self, expense_id):
"""Delete expense by ID"""
with self.get_connection() as conn:
conn.execute('DELETE FROM expenses WHERE id = ?', (expense_id,))
conn.commit()
# Initialize database
db = DatabaseManager()
@app.route('/')
def index():
"""Home page showing recent expenses"""
expenses = db.get_expenses(limit=10)
summary = db.get_expense_summary()
total_expenses = sum(row['total'] for row in summary)
return render_template('index.html',
expenses=expenses,
summary=summary,
total=total_expenses)
@app.route('/add', methods=['GET', 'POST'])
def add_expense():
"""Add new expense"""
if request.method == 'POST':
description = request.form['description'].strip()
amount = float(request.form['amount'])
category = request.form['category']
date = request.form['date']
if description and amount > 0:
db.add_expense(description, amount, category, date)
flash('Expense added successfully!', 'success')
return redirect(url_for('index'))
else:
flash('Please fill in all fields correctly.', 'error')
return render_template('add_expense.html')
@app.route('/api/expenses')
def api_expenses():
"""API endpoint for expenses data"""
expenses = db.get_expenses()
return jsonify([dict(row) for row in expenses])
@app.route('/delete/<int:expense_id>')
def delete_expense(expense_id):
"""Delete expense"""
db.delete_expense(expense_id)
flash('Expense deleted successfully!', 'success')
return redirect(url_for('index'))
# HTML Templates (save as templates/base.html)
base_template = '''
<!DOCTYPE html>
<html>
<head>
<title>Expense Tracker</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
.expense-item { border: 1px solid #ddd; padding: 10px; margin: 10px 0; }
.btn { padding: 8px 16px; background: #007bff; color: white; text-decoration: none; border-radius: 4px; }
.btn-danger { background: #dc3545; }
.alert { padding: 10px; margin: 10px 0; border-radius: 4px; }
.alert-success { background: #d4edda; color: #155724; }
.alert-error { background: #f8d7da; color: #721c24; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
</style>
</head>
<body>
<div class="container">
<h1>Personal Expense Tracker</h1>
<nav>
<a href="{{ url_for('index') }}" class="btn">Home</a>
<a href="{{ url_for('add_expense') }}" class="btn">Add Expense</a>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</body>
</html>
'''
if __name__ == '__main__':
# Create templates directory and files
import os
os.makedirs('templates', exist_ok=True)
with open('templates/base.html', 'w') as f:
f.write(base_template)
# Create additional template files...
app.run(debug=True)
Project Integration and Deployment
These projects demonstrate different aspects of Python development:
- Task Manager: File I/O, OOP, command-line interfaces
- News Analyzer: Web scraping, data analysis, visualization
- Web Application: Web frameworks, databases, templates
To deploy these projects:
- Create virtual environments for each project
- Add requirements.txt files listing dependencies
- Include error handling and logging
- Add configuration files for different environments
- Write tests for critical functionality
- Document installation and usage instructions
Next Steps
These projects provide a foundation for more advanced Python development:
- Add databases to the task manager (SQLite, PostgreSQL)
- Implement user authentication in the web application
- Add API endpoints for mobile app integration
- Deploy to cloud platforms (Heroku, AWS, DigitalOcean)
- Add automated testing and continuous integration
- Implement caching and performance optimizations
The key to becoming proficient in Python is building projects that solve real problems. Start with simple versions, then gradually add features and complexity. Each project teaches you something new about Python and software development in general.
You now have the fundamentals, techniques, and practical experience to build meaningful Python applications. The journey from here is about applying these skills to increasingly complex and interesting problems.