Python Complete Guide: Learn Python From Beginner to Advanced
Learn Python from scratch with this complete guide covering variables, data types, loops, functions, OOP, file handling, APIs, databases, testing, automation, security, and advanced Python concepts, with exercises after every major topic.
Learn Python from scratch, understand what happens when your code runs, and progress toward professional software development with practical examples, exercises, and real-world projects.
Python is one of the most approachable programming languages for beginners, but its simplicity can be misleading. Writing a few lines of Python is easy. Understanding how to structure larger programs, work with files and databases, handle errors, write tests, manage dependencies, and build reliable applications takes a deeper understanding of the language.
This guide is designed to take you through that entire progression.
You do not need previous programming experience. You do not need to know what a variable, function, class, API, or database is before you begin. Each concept is introduced from the ground up, explained in plain language, and connected to examples you can run yourself.
The goal is not to memorize every Python feature. It is to understand how programming works, learn how to solve problems using Python, and build the skills needed to work independently on real projects.
By the end, you should have a strong foundation in:
Python syntax, data types, variables, and control flow.
Strings, lists, tuples, dictionaries, sets, and data transformations.
Functions, scope, closures, modules, and reusable code.
Error handling, debugging, files, and data formats.
Object-oriented programming and Python's underlying object model.
Iterators, generators, decorators, context managers, and type hints.
Virtual environments, packages, testing, and professional tooling.
APIs, databases, automation, concurrency, performance, and security.
Building, testing, and organizing complete Python applications.
The article includes exercises after each major section. Try solving them before reading the suggested solutions or checking the expected results.
Part 1: What Python Actually Is
1.1 What is programming?
Imagine you want a computer to calculate the total price of three products.
You could perform the calculation yourself:
Take the price of the first product.
Add the price of the second.
Add the price of the third.
Display the result.
Programming means describing these instructions precisely enough that a computer can execute them.
A program is a set of instructions that performs a task.
Python is a language in which you can express those instructions.
For example:
first_price = 100
second_price = 250
third_price = 150
total = first_price + second_price + third_price
print(total)Output:
500The computer does not understand the intention behind your code in the way a human might. It follows the rules of the programming language and the instructions represented by your program.
This is why precision matters. Computers will execute incorrect instructions perfectly if those instructions are valid.
1.2 Why learn Python?
Python is designed to be readable and has a large ecosystem of libraries.
It is used for:
Area | What Python can do |
|---|---|
Automation | Rename files, process documents, organize folders, and automate repetitive work |
Web development | Build websites, backend APIs, and server applications |
Data analysis | Clean datasets, calculate statistics, and produce reports |
Artificial intelligence | Train models, process data, and work with machine-learning frameworks |
Scientific computing | Perform numerical calculations, simulations, and experiments |
Cybersecurity | Analyze logs, inspect systems, and automate authorized security tasks |
DevOps | Manage deployment scripts, cloud infrastructure, and development workflows |
Desktop applications | Build graphical applications and utilities |
Testing | Test software automatically and generate reports |
One important distinction: Python is the language, while libraries provide additional capabilities.
For example, the language itself gives you variables, functions, loops, and data structures. A library such as pandas provides specialized tools for manipulating tabular data.
You can start with the language's built-in features and introduce external libraries as your projects require them.
1.3 How Python runs your code
Python is commonly described as an interpreted language, but the actual execution process involves several steps.
Consider this program:
price = 100
quantity = 3
total = price * quantity
print(total)Conceptually, Python:
Reads and parses the source code.
Checks whether the code follows Python's syntax rules.
Compiles the source into an internal representation called bytecode in the usual CPython implementation.
Executes that bytecode using the Python runtime.
Performs operations such as multiplication and printing.
You normally do not need to manage these steps yourself.
The important point is that Python source code must follow the language's rules, and the interpreter executes the resulting program.
If your code contains invalid syntax, Python cannot execute it as intended. If the syntax is valid but your logic is wrong, the program may run and produce the wrong answer.
These are two different kinds of problems, and learning to distinguish them is fundamental to debugging.
1.4 Your first Python program
Create a file named hello.py:
print("Hello, world!")Run it from your terminal:
python hello.pyOn some systems, the command may be:
python3 hello.pyOn Windows, this command is also commonly available:
py hello.pyThe exact command depends on how Python was installed.
Expected output:
Hello, world!The print() function displays information in the program's output.
You can also print several values:
print("My name is Alex")
print("I am learning Python")
print(10 + 5)Output:
My name is Alex
I am learning Python
15Notice that Python evaluates 10 + 5 before displaying the result. The code does not print the characters 10 + 5; it prints the result of the expression.
Exercises: Your first programs
Exercise 1: Write a program that prints your name, your country, and the programming language you are learning.
Exercise 2: Write a program that prints the result of 25 + 17, 100 - 35, 12 * 8, and 144 / 12.
Exercise 3: Print a three-line introduction using three separate print() statements.
Expected results: Your program should display the values you intended, with the operations producing 42, 65, 96, and 12.0, respectively.
Part 2: Python Syntax and Indentation
2.1 What is syntax?
Syntax is the set of rules governing how code must be written.
In English, a sentence normally follows certain grammatical rules. In Python, instructions must also follow a defined structure.
This is valid:
name = "Alex"
print(name)This is not:
if True
print("Hello")The second example is missing a colon after True, so Python raises a SyntaxError.
Correct version:
if True:
print("Hello")The colon indicates that a block of code follows.
2.2 Indentation is part of Python's syntax
Many languages use braces to define code blocks. Python primarily uses indentation.
age = 20
if age >= 18:
print("You are an adult")
print("You meet the age condition")
print("Program finished")The first two print() statements are indented and belong to the if block.
The final statement is not indented, so it belongs to the outer level of the program.
The output is:
You are an adult
You meet the age condition
Program finishedCompare it with:
age = 16
if age >= 18:
print("You are an adult")
print("Program finished")Output:
Program finishedThe conditional message is skipped because the condition is false.
Use four spaces for each indentation level, which is the standard Python convention. Avoid mixing tabs and spaces.
2.3 Comments
Comments explain the code to human readers. Python ignores comments during ordinary execution.
# Calculate the total price.
price = 100
quantity = 3
total = price * quantityComments can be useful, but do not explain every obvious instruction.
Prefer a comment that explains why something is being done rather than repeating what the code already says.
# Keep a small buffer because the external service limits requests.
retry_buffer = 2This is more useful than:
# Set retry buffer to 2.
retry_buffer = 22.4 Statements and expressions
A statement performs an action, such as assigning a value or starting a loop.
name = "Alex"An expression produces a value.
10 + 5Many Python statements contain expressions:
total = 10 + 5Here, the expression 10 + 5 produces a value, and the assignment statement binds that value to the name total.
Understanding the difference helps when learning functions, conditions, and return values.
Exercises: Syntax
Exercise 1: Create a program with an if statement that prints a message when a number is greater than 10.
Exercise 2: Add a comment explaining a calculation.
Exercise 3: Intentionally remove a colon from an if statement, run your program, and read the error. Restore the colon and run it again.
The purpose of the third exercise is to become comfortable with errors. Every developer encounters them.
Part 3: Variables and Assignment
3.1 What is a variable?
A variable is a name that refers to a value.
name = "Alex"
age = 25You can think of the name age as a label that lets you refer to the value 25.
Technically, Python names are bindings to objects. This distinction becomes important when you begin working with lists and dictionaries, but the label analogy is useful for a beginner.
You can use a variable wherever you would otherwise use its value:
price = 100
quantity = 3
total = price * quantity
print(total)Output:
300The program calculates 100 * 3 and binds the result to total.
3.2 Variables can be reassigned
score = 10
print(score)
score = 20
print(score)Output:
10
20The second assignment makes score refer to a different integer value.
Unlike some other languages, Python does not require you to declare a variable's type in advance.
value = 10
value = "Hello"
value = TrueThis is valid Python. However, repeatedly changing a variable's conceptual role can make a program confusing, so use it deliberately.
3.3 Assigning multiple variables
Python supports multiple assignment:
name, age, country = "Alex", 25, "India"It also supports assigning the same value to several names:
first = second = third = 0For variables that can change independently, avoid using chained assignment with mutable objects. It makes several names refer to the same object.
first = second = []
first.append("Python")
print(second) # ['Python']Both names refer to one list. This can be surprising if you intended to create two separate lists.
Instead:
first = []
second = []3.4 Naming variables
Python's conventional naming style is snake_case.
first_name = "Alex"
article_count = 12
maximum_attempts = 3Names are case-sensitive:
score = 10
Score = 20
print(score) # 10
print(Score) # 20Choose descriptive names. article_count is generally more useful than ac.
Avoid reserved Python keywords, such as if, for, class, and return, as variable names.
Exercises: Variables
Exercise 1: Create variables for a product's name, price, and quantity. Calculate the total cost.
Exercise 2: Assign two numbers to variables. Swap their values using Python's multiple assignment:
a, b = b, aExercise 3: Create two separate empty lists and demonstrate that modifying one does not modify the other.
Expected result for Exercise 1: A product priced at 250 with a quantity of 4 should have a total cost of 1000.
Part 4: Data Types
Python values have types. A type determines which operations are meaningful for a value.
For example, adding two numbers means arithmetic addition. Adding two strings means concatenation.
4.1 Integers
Integers represent whole numbers.
age = 25
temperature = -5
number_of_articles = 1000Python integers can grow beyond ordinary machine-sized integer limits, subject to available memory.
4.2 Floating-point numbers
Floats represent numbers with fractional parts.
price = 19.99
temperature = 36.5Floating-point arithmetic can produce small precision errors because many decimal fractions cannot be represented exactly in binary.
print(0.1 + 0.2)You may see:
0.30000000000000004This is not a Python mistake. It is a characteristic of binary floating-point arithmetic.
For approximate numerical comparisons, use a tolerance:
import math
result = 0.1 + 0.2
print(math.isclose(result, 0.3))For applications involving exact money calculations, consider storing values in integer minor units or using Python's decimal.Decimal type.
from decimal import Decimal
price = Decimal("19.99")
tax = Decimal("1.50")
print(price + tax)When using Decimal, construct values from strings or other suitable exact representations rather than first creating an imprecise float.
4.3 Strings
Strings represent text.
name = "Alex"
message = 'Learning Python'Both single and double quotes work. Choose whichever makes the string easier to read.
message = "It's a useful skill"
quote = 'She said, "Hello."'Strings can contain numbers as text:
age = "25"Although the characters look like a number, the value is a string. You cannot safely assume it will behave like an integer.
4.4 Booleans
Booleans have only two values:
is_published = True
is_admin = FalsePython uses capitalized True and False.
Booleans are used for conditions, comparisons, and logical operations.
4.5 None
None represents the absence of a value.
selected_article = NoneFor example, a search operation might return None when it cannot find a matching record.
Test for the absence of a value using identity:
if selected_article is None:
print("No article selected")Use is None, not == None, as the standard Python convention.
4.6 Checking types
Use type() to inspect a value:
print(type(25))
print(type(19.99))
print(type("Hello"))
print(type(True))
print(type(None))You can use isinstance() to check whether a value belongs to a type:
age = 25
print(isinstance(age, int))isinstance() is generally more appropriate for validation because it also recognizes relevant inheritance relationships.
An interesting Python detail: bool is a subclass of int.
print(isinstance(True, int)) # TrueFor ordinary programming, you usually do not need to worry about this immediately, but it is useful to know when writing strict type validation.
4.7 Type conversion
Convert values explicitly when necessary.
age_text = "25"
age = int(age_text)
price_text = "19.99"
price = float(price_text)
number = 100
text = str(number)Invalid conversions raise errors:
# int("hello") # ValueErrorDo not assume arbitrary user input can be converted successfully. Handle invalid input when appropriate.
4.8 Type conversion versus validation
Consider:
age = int(input("Enter your age: "))This works when the user enters something like 25. It fails if the user enters twenty-five.
Conversion tries to turn a value into another type. Validation determines whether the input meets your application's requirements.
These are related but different responsibilities.
Exercises: Data types
Exercise 1: Create one example each of an integer, float, string, Boolean, and None.
Exercise 2: Convert the string "450" into an integer and add 50.
Exercise 3: Ask the user to enter a number and display its square. Handle a non-numeric input without crashing.
Exercise 4: Explain why "10" + "5" produces "105" while 10 + 5 produces 15.
Suggested solution for Exercise 3
try:
number = float(input("Enter a number: "))
print("Square:", number ** 2)
except ValueError:
print("Please enter a valid number.")This will be explained in more depth when we reach exception handling.
Part 5: Operators and Expressions
Operators perform calculations, comparisons, assignments, and logical operations.
5.1 Arithmetic operators
print(10 + 3) # 13
print(10 - 3) # 7
print(10 * 3) # 30
print(10 / 3) # 3.333...
print(10 // 3) # 3
print(10 % 3) # 1
print(2 ** 3) # 8The operators are:
+addition.-subtraction.*multiplication./division.//floor division.%remainder.**exponentiation.
Floor division rounds down toward negative infinity, not simply toward zero.
print(-7 // 2) # -4This differs from truncating the result toward zero.
5.2 Comparison operators
age = 25
print(age == 25)
print(age != 18)
print(age > 18)
print(age < 30)
print(age >= 25)
print(age <= 40)Comparisons produce Boolean results.
Be careful to distinguish assignment (=) from equality comparison (==).
5.3 Logical operators
Python uses words rather than symbolic operators for logical conjunction and disjunction:
is_logged_in = True
is_admin = False
print(is_logged_in and is_admin)
print(is_logged_in or is_admin)
print(not is_logged_in)andrequires both conditions to be truthy.orrequires at least one condition to be truthy.notreverses truthiness.
These operators short-circuit. For example, A and B does not need to evaluate B if A is falsy.
5.4 Operator precedence
Python follows rules that determine the order of operations.
result = 2 + 3 * 4
print(result) # 14Multiplication happens before addition.
Parentheses make your intended order explicit:
result = (2 + 3) * 4
print(result) # 20Prefer readable expressions over relying on a reader to remember every precedence rule.
5.5 Membership and identity
Membership operators check whether a value appears in a collection or sequence:
print("py" in "python")
print(3 in [1, 2, 3])The is operator checks object identity: whether two references point to the same object.
The == operator checks equality according to the object's comparison behavior.
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True
print(a is b) # False
print(a is c) # TrueUse == to compare values. Use is when identity is what matters, most commonly when checking against None.
Exercises: Operators
Exercise 1: Build a simple calculator using two numbers and the four basic arithmetic operations.
Exercise 2: Check whether a number is divisible by both 3 and 5.
Exercise 3: Write a condition that checks whether a user is logged in and has administrative permission.
Exercise 4: Create two separate lists containing the same values and demonstrate the difference between == and is.
Part 6: User Input and Output
6.1 The input() function
input() pauses the program until the user enters text and presses Enter.
name = input("What is your name? ")
print("Hello,", name)A key detail: input() returns a string.
age = input("Enter your age: ")
print(type(age)) # <class 'str'>Convert the result when a number is needed:
age = int(input("Enter your age: "))
print(age + 1)The program fails if the input cannot be converted to an integer. Validation will help you handle that situation.
6.2 Formatting output
You can use f-strings to insert values into text.
name = "Alex"
age = 25
print(f"My name is {name} and I am {age} years old.")Expressions can appear inside the braces:
price = 100
quantity = 3
print(f"Total: {price * quantity}")F-strings also support formatting:
price = 19.9876
print(f"{price:.2f}") # 19.99The .2f format requests two digits after the decimal point.
6.3 Getting reliable user input
A simple input loop can allow users to recover from invalid entries:
while True:
try:
age = int(input("Enter your age: "))
if age < 0:
print("Age cannot be negative.")
continue
break
except ValueError:
print("Enter a whole number.")
print(f"Your age is {age}.")This program performs three separate jobs:
It collects input.
It converts the input and handles conversion errors.
It enforces a business rule: age must not be negative.
Separating these concerns makes programs easier to extend.
Exercises: Input and output
Exercise 1: Ask for a name and display a personalized greeting.
Exercise 2: Ask for the price and quantity of a product and display the total to two decimal places.
Exercise 3: Ask for a temperature in Celsius and convert it to Fahrenheit using:
F = C × 9 / 5 + 32
Exercise 4: Improve the temperature converter so it handles invalid input without terminating unexpectedly.
Part 7: Conditional Statements
A conditional statement allows a program to make decisions.
7.1 if
age = 20
if age >= 18:
print("You are an adult")The indented block runs only when the condition evaluates to true.
7.2 if and else
age = 16
if age >= 18:
print("Adult")
else:
print("Minor")Only one branch is selected.
7.3 elif
Use elif to evaluate additional conditions:
score = 82
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "Needs improvement"
print(grade)Python checks the conditions from top to bottom and executes the first branch whose condition is true.
The order matters. If you check score >= 70 first, a score of 95 would satisfy it and never reach the higher-grade conditions.
7.4 Truthiness
Conditions do not require explicit Boolean expressions.
name = ""
if name:
print("Name provided")
else:
print("Name missing")An empty string is falsy.
Common falsy values include False, None, numeric zero, and empty collections such as [], {}, and set().
Non-empty strings, including "0", are truthy.
Do not confuse truthiness with validation. A non-empty string is not necessarily a valid email address or a legitimate user ID.
7.5 Structural pattern matching
Python 3.10 and later support match and case.
status = "published"
match status:
case "draft":
print("Not public")
case "published":
print("Available to readers")
case "archived":
print("Stored in archive")
case _:
print("Unknown status")The underscore is a catch-all pattern.
This feature can be useful when matching structured values or several alternatives. For straightforward conditions, ordinary if statements are often simpler.
Exercises: Conditions
Exercise 1: Write a program that determines whether an integer is positive, negative, or zero.
Exercise 2: Ask for a score and assign a grade using at least four ranges.
Exercise 3: Ask for a user's age and determine whether they meet a minimum age of 18.
Exercise 4: Write a login simulation that accepts a username and password and prints whether they match predefined example credentials. Treat it as a learning exercise, not a production authentication system.
Part 8: Loops and Repetition
Loops allow you to execute code repeatedly without copying the same instructions many times.
8.1 The for loop
for number in [1, 2, 3, 4, 5]:
print(number)Python retrieves each item from the iterable and runs the loop body once for that item.
An iterable is an object that can provide values one at a time. Lists, strings, tuples, sets, and dictionaries are examples.
8.2 The range() function
range() generates a sequence of integers that a loop can consume.
for number in range(5):
print(number)Output:
0
1
2
3
4The stopping value is excluded.
You can specify a starting value and a step:
for number in range(2, 11, 2):
print(number)Output:
2
4
6
8
108.3 The while loop
A while loop repeats as long as its condition is truthy.
count = 1
while count <= 5:
print(count)
count += 1The condition eventually becomes false, allowing the loop to stop.
If the condition never becomes false and there is no other exit, you can create an infinite loop.
8.4 break and continue
break exits a loop.
for number in range(10):
if number == 5:
break
print(number)continue skips the remainder of the current iteration and moves to the next one.
for number in range(5):
if number == 2:
continue
print(number)Output:
0
1
3
4Use these statements when they make control flow clearer. Excessive use of nested breaks and continues can make a program harder to follow.
8.5 Looping through dictionaries
user = {
"name": "Alex",
"age": 25,
"role": "Developer",
}
for key, value in user.items():
print(key, value)The items() method provides key-value pairs.
Use keys() when you need keys and values() when you need values.
8.6 Nested loops
A nested loop is a loop inside another loop.
for row in range(3):
for column in range(3):
print(row, column)The inner loop runs completely for each iteration of the outer loop.
Nested loops are useful for grids, tables, and pairwise comparisons. However, processing every pair of items in a large collection can be expensive because the number of operations grows rapidly.
Exercises: Loops
Exercise 1: Print the numbers from 1 to 100.
Exercise 2: Print only the even numbers between 1 and 50.
Exercise 3: Calculate the sum of all numbers from 1 to 100.
Exercise 4: Print a multiplication table for a number supplied by the user.
Exercise 5: Count how many vowels appear in a string.
Exercise 6: Print a right triangle of asterisks with five rows.
Hints
For Exercise 2, check the remainder when dividing by 2.
For Exercise 3, begin with a total of zero and add each number to it.
For Exercise 5, remember that a string can be iterated one character at a time.
Part 9: Strings in Depth
Strings are one of the most frequently used types in real applications because programs constantly handle names, filenames, messages, identifiers, and user input.
9.1 Indexing
Each character in a string has a position.
word = "Python"
print(word[0]) # P
print(word[1]) # y
print(word[-1]) # nPositive indexes begin at zero. Negative indexes count backward from the end.
Strings are immutable. You cannot assign a different character directly into an existing string.
9.2 Slicing
Slicing extracts part of a sequence.
word = "Python"
print(word[0:3]) # Pyt
print(word[2:]) # thon
print(word[:4]) # Pyth
print(word[::-1]) # nohtyPThe ending index is excluded.
Slicing is useful for extracting substrings, taking portions of lists, and working with other sequence types.
9.3 Useful string methods
text = " Learning Python "
print(text.strip())
print(text.lower())
print(text.upper())
print(text.replace("Python", "Programming"))
print(text.startswith(" Learn"))
print(text.count("n"))Other useful methods include split(), join(), find(), endswith(), and casefold().
strip() removes leading and trailing whitespace, not every whitespace character inside the string.
9.4 Splitting and joining
sentence = "Python is useful"
words = sentence.split()
print(words)Output:
['Python', 'is', 'useful']The reverse operation joins strings:
words = ["Python", "is", "useful"]
sentence = " ".join(words)
print(sentence)Output:
Python is usefulThe string before .join() is the separator used between elements.
9.5 String formatting
F-strings are usually the clearest way to insert values into text.
name = "Alex"
score = 95
message = f"{name} scored {score}%."
print(message)They support formatting values:
price = 1234.5
print(f"Price: {price:.2f}")
print(f"Price: {price:,.2f}")Output:
Price: 1234.50
Price: 1,234.509.6 Unicode and real-world text
Python strings represent Unicode text. A visible character can consist of multiple code points, so counting code points is not always the same as counting user-perceived characters.
This matters when building text editors, applying strict text limits, or processing international text.
For ordinary text manipulation, Python's string methods are often sufficient. More specialized applications may need Unicode normalization or grapheme-aware processing.
Exercises: Strings
Exercise 1: Ask for a sentence and count its characters.
Exercise 2: Convert a sentence to lowercase and count how many times a chosen letter appears.
Exercise 3: Reverse a string using slicing.
Exercise 4: Ask for a full name and display the first and last names separately.
Exercise 5: Normalize an email-like input by removing surrounding whitespace and converting it to lowercase. Explain why this alone is not complete email validation.
Part 10: Lists
A list is an ordered, mutable collection. It can contain values of different types, although using one consistent type is often clearer.
10.1 Creating lists
languages = ["Python", "JavaScript", "Go"]
numbers = [10, 20, 30, 40]
mixed = ["Alex", 25, True]Access elements by index:
print(languages[0])
print(languages[-1])Find its length:
print(len(languages))10.2 Modifying lists
languages = ["Python", "JavaScript"]
languages.append("Go")
languages.insert(1, "Rust")
print(languages)Remove values:
languages.remove("Rust")
last_item = languages.pop()remove() removes the first matching value and raises ValueError if it does not find one. pop() removes and returns an item, raising IndexError if the requested position does not exist.
Check membership before removing an optional value:
if "Rust" in languages:
languages.remove("Rust")10.3 Sorting lists
numbers = [5, 2, 9, 1]
numbers.sort()
print(numbers)sort() modifies the list.
To produce a sorted list without modifying the original:
numbers = [5, 2, 9, 1]
sorted_numbers = sorted(numbers)Sort strings or objects using key when needed:
names = ["Alex", "Christopher", "Sam"]
names.sort(key=len)
print(names)10.4 Copying lists
This assignment does not create a new list:
first = [1, 2, 3]
second = firstBoth names refer to the same object.
second.append(4)
print(first) # [1, 2, 3, 4]Make a shallow copy using slicing or .copy():
second = first.copy()A shallow copy is sufficient for a flat list of immutable values. If the list contains nested mutable objects, those objects can still be shared.
10.5 List comprehensions
A list comprehension creates a list from an iterable.
squares = [number ** 2 for number in range(1, 6)]
print(squares)Output:
[1, 4, 9, 16, 25]Add a condition:
even_squares = [
number ** 2
for number in range(1, 11)
if number % 2 == 0
]Comprehensions are concise and useful, but complex nested comprehensions can become difficult to read. A normal loop is often better when multiple steps or error conditions are involved.
Exercises: Lists
Exercise 1: Create a list of five numbers and calculate its sum and average.
Exercise 2: Find the largest and smallest values in a list.
Exercise 3: Remove duplicates from a list while preserving the original order.
Exercise 4: Create a list containing the squares of all odd numbers from 1 to 25.
Exercise 5: Sort a list of names by their length.
Part 11: Tuples, Sets, and Dictionaries
Python provides several collections because different problems require different behaviors.
11.1 Tuples
A tuple is an ordered collection that cannot have its elements reassigned through ordinary item assignment.
coordinates = (10, 20)
print(coordinates[0])Tuples are useful for fixed groups of values, such as coordinates or a result consisting of two related values.
Tuple unpacking:
x, y = coordinatesA single-element tuple requires a comma:
single = (5,)Without the comma, (5) is simply the integer 5.
A tuple is immutable, but an object stored inside it might itself be mutable. Therefore, tuple immutability does not guarantee that every object reachable through a tuple is immutable.
11.2 Sets
A set stores unique hashable values.
tags = {"python", "web", "data", "python"}
print(tags)The duplicate "python" is stored only once.
Sets are useful for membership checks and removing duplicates.
first = {1, 2, 3}
second = {3, 4, 5}
print(first | second) # Union
print(first & second) # Intersection
print(first - second) # DifferenceSets are not ordered sequences. Do not rely on a particular display order.
An empty set is created with set(), not {}, because {} creates an empty dictionary.
11.3 Dictionaries
A dictionary maps keys to values.
user = {
"name": "Alex",
"age": 25,
"role": "Developer",
}Retrieve values:
print(user["name"])Accessing a missing key with square brackets raises KeyError.
Use .get() when an optional key may be missing:
print(user.get("country"))
print(user.get("country", "Unknown"))Add or update a value:
user["age"] = 26
user["country"] = "India"Remove an entry:
del user["role"]11.4 Iterating over dictionaries
for key in user:
print(key)To retrieve keys and values:
for key, value in user.items():
print(key, value)Dictionaries preserve insertion order in modern Python, but you should still choose the appropriate data structure based on its purpose.
11.5 Dictionary comprehensions
squares = {
number: number ** 2
for number in range(1, 6)
}You can transform or filter dictionaries using comprehensions, but keep them simple enough to understand at a glance.
11.6 Choosing the right data structure
Need | Suitable starting point |
|---|---|
Ordered, changeable collection | List |
Fixed collection of values | Tuple |
Unique values and membership checks | Set |
Key-value relationships | Dictionary |
Text | String |
The right data structure can make code both clearer and more efficient.
Exercises: Collections
Exercise 1: Store a person's name, age, and city in a dictionary.
Exercise 2: Count the frequency of each word in a sentence using a dictionary.
Exercise 3: Given two lists of tags, find the tags that appear in both.
Exercise 4: Remove duplicates from a collection and explain whether order matters for your solution.
Exercise 5: Represent a shopping cart as a dictionary that maps product names to quantities. Calculate the total using a separate dictionary of prices.
Part 12: Functions and Reusable Code
Functions are one of the most important building blocks in Python. They allow you to group logic into named operations that can be called repeatedly.
12.1 Defining a function
def greet():
print("Hello!")This defines the function. It does not run the body immediately.
Call it:
greet()Output:
Hello!A function definition begins with def, followed by the function name, parentheses, and a colon.
12.2 Parameters and arguments
def greet(name):
print(f"Hello, {name}!")
greet("Alex")
greet("Sam")name is a parameter. "Alex" and "Sam" are arguments supplied when calling the function.
Functions can accept multiple arguments:
def add(a, b):
return a + b
result = add(10, 20)
print(result)12.3 Return values
A function can produce a result using return.
def square(number):
return number * number
result = square(5)
print(result)This differs from printing inside a function:
def square_and_print(number):
print(number * number)The second function displays a result but does not return it explicitly. Its return value is None.
A reliable rule is:
Use
print()to display information.Use
returnto pass a value back to the caller.
Functions that return results are generally easier to combine and test.
12.4 Default parameters
def greet(name="Guest"):
return f"Hello, {name}!"
print(greet())
print(greet("Alex"))Use defaults for sensible fallback values.
A particularly important mistake is using a mutable object as a default parameter:
def add_item(item, items=[]):
items.append(item)
return itemsThe default list is created once when the function is defined and is reused across calls.
A safer approach is:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsThis creates a new list when no list is supplied.
12.5 Keyword arguments
def create_user(name, age, country):
return {
"name": name,
"age": age,
"country": country,
}
user = create_user(
name="Alex",
age=25,
country="India",
)Keyword arguments make calls easier to understand, especially when a function has several parameters.
12.6 *args and **kwargs
*args collects additional positional arguments into a tuple.
def add_all(*numbers):
return sum(numbers)
print(add_all(1, 2, 3, 4))**kwargs collects additional keyword arguments into a dictionary.
def show_details(**details):
for key, value in details.items():
print(key, value)
show_details(name="Alex", role="Developer")These features are useful for flexible APIs and wrappers. Avoid using them unnecessarily when explicit parameters would make a function clearer.
12.7 Scope and local variables
A variable assigned inside a function is normally local to that function.
def calculate():
result = 10 + 5
return result
print(calculate())The name result is not automatically available outside the function.
Functions can read variables from an enclosing scope, but changing shared state can make behavior harder to understand. Prefer passing required values as arguments and returning results whenever practical.
12.8 Lambda functions
A lambda creates a small anonymous function.
double = lambda number: number * 2
print(double(5))Lambdas are most useful for short expressions, such as sort keys:
names = ["Alex", "Christopher", "Sam"]
names.sort(key=lambda name: len(name))For complicated logic, use a normal def function with a descriptive name.
12.9 Recursion
A recursive function calls itself.
def factorial(number):
if number < 0:
raise ValueError("Number must be non-negative")
if number <= 1:
return 1
return number * factorial(number - 1)The base case prevents infinite recursion. For large inputs or deeply nested structures, iterative approaches may be more appropriate because Python has a finite recursion limit.
Exercises: Functions
Exercise 1: Write a function that accepts two numbers and returns their sum.
Exercise 2: Write a function that checks whether a number is even.
Exercise 3: Write a function that accepts a list and returns its largest number. Decide how your function should behave for an empty list.
Exercise 4: Write a function that accepts a price and a discount percentage and returns the discounted price.
Exercise 5: Write a function with a default parameter and demonstrate what happens when the argument is omitted.
Exercise 6: Explain the difference between a function that prints a value and a function that returns a value.
Part 13: Mutability, References, and Python's Object Model
Understanding objects and references explains many otherwise confusing Python behaviors.
13.1 Names refer to objects
Consider:
a = [1, 2, 3]
b = aThis does not create two independent lists. Both names refer to the same list.
b.append(4)
print(a) # [1, 2, 3, 4]The list was modified, so either name sees the change.
13.2 Mutable and immutable objects
Common immutable types include:
Integers.
Floats.
Booleans.
Strings.
Tuples, as containers of their existing element references.
None.
Common mutable types include:
Lists.
Dictionaries.
Sets.
Most user-defined objects.
Immutability concerns whether an object itself can be changed, not whether a variable can be reassigned.
name = "Alex"
name = "Sam"The variable name is reassigned to a different string. The original string is not modified.
13.3 Copying objects
A shallow copy duplicates the outer collection but keeps references to nested objects.
original = {
"name": "Alex",
"skills": ["Python", "SQL"],
}
copy = original.copy()
copy["name"] = "Sam"
copy["skills"].append("Git")
print(original["name"]) # Alex
print(original["skills"]) # ['Python', 'SQL', 'Git']The dictionary was copied, but the nested list remained shared.
For suitable objects, use copy.deepcopy() when an independent nested structure is required:
import copy
independent_copy = copy.deepcopy(original)Deep copying is not automatically appropriate for every object. Files, network connections, external resources, and custom objects may need a different strategy.
13.4 Passing mutable objects to functions
def add_skill(user):
user["skills"].append("Git")
person = {
"name": "Alex",
"skills": ["Python"],
}
add_skill(person)
print(person)The function modifies the shared nested list.
Python passes object references through argument values. Mutating an object can affect other parts of the program that refer to it, whereas rebinding a local parameter does not rebind the caller's variable.
Understanding this distinction helps prevent state-related bugs.
Exercises: References
Exercise 1: Create two names that refer to the same list and show that changing it through one name affects the other.
Exercise 2: Copy a list using .copy() and show that appending to the copy does not affect the original.
Exercise 3: Create a dictionary containing a nested list, make a shallow copy, and demonstrate that the nested list is shared.
Exercise 4: Repeat Exercise 3 using copy.deepcopy() and explain the difference.
Part 14: Exceptions and Error Handling
Errors happen because of invalid input, unexpected conditions, network problems, missing files, or bugs. Professional code anticipates reasonable failures and responds appropriately.
14.1 Common exception types
Examples include:
SyntaxError— the code violates Python's syntax rules.NameError— a name cannot be found.TypeError— an operation receives an incompatible type.ValueError— the type is appropriate, but the value is invalid for the operation.IndexError— a sequence index is out of range.KeyError— a dictionary key is missing.FileNotFoundError— a requested file does not exist.ZeroDivisionError— a number is divided by zero.
Learn to read the exception type, message, and traceback rather than treating every error as mysterious.
14.2 try and except
try:
age = int(input("Enter your age: "))
print(f"Next year, you will be {age + 1}.")
except ValueError:
print("Please enter a whole number.")The try block contains the operation that may fail. The except block handles a specific exception.
Catch the exceptions you can meaningfully handle. Avoid catching every possible exception without a recovery plan.
14.3 else and finally
try:
number = int(input("Enter a number: "))
except ValueError:
print("Invalid input")
else:
print("Your number is:", number)
finally:
print("Input operation finished")else executes if the try block completes without an exception. finally executes as the code leaves the construct, even when an exception or control-flow operation causes the block to exit.
finally is useful for cleanup, but resource management is often clearer with context managers, which are covered later.
14.4 Raising exceptions
Your own functions can signal invalid operations:
def calculate_discount(price, percentage):
if price < 0:
raise ValueError("Price cannot be negative")
if not 0 <= percentage <= 100:
raise ValueError("Discount must be between 0 and 100")
return price * (1 - percentage / 100)Raise meaningful exceptions when an operation cannot continue under the given conditions.
Do not use exceptions as a replacement for every ordinary conditional decision. Use them to report exceptional or invalid situations.
14.5 Debugging with tracebacks
A traceback shows the chain of calls that led to an exception.
When debugging:
Read the exception type and message.
Identify the last relevant line in your code.
Inspect the variables involved.
Check where those values originated.
Make the smallest correction that fixes the underlying problem.
Add a test for important bugs.
Do not simply suppress the error and continue if the application may now be in an invalid state.
Exercises: Error handling
Exercise 1: Ask the user for an integer and handle invalid input.
Exercise 2: Write a division function that handles division by zero in a deliberate way.
Exercise 3: Raise a ValueError if a supplied age is negative.
Exercise 4: Write a loop that continues asking for a valid number until the user supplies one.
Part 15: Working With Files
Programs often need to store or retrieve information. Files make data persist after the program exits.
15.1 Opening a file
file = open("notes.txt", "r", encoding="utf-8")
contents = file.read()
file.close()The "r" mode means read.
The problem with the pattern above is that the file may remain open if an exception interrupts execution before close() runs.
Python provides a better approach.
15.2 Use with
with open("notes.txt", "r", encoding="utf-8") as file:
contents = file.read()
print(contents)The with statement uses a context manager to ensure the file is properly closed when the block exits.
This is the recommended pattern for normal file handling.
15.3 Writing to files
with open("notes.txt", "w", encoding="utf-8") as file:
file.write("Learning Python\n")
file.write("Practicing file operations\n")The "w" mode creates a file or overwrites an existing file. Be careful: opening an existing file in write mode can erase its contents.
Use append mode to add content:
with open("notes.txt", "a", encoding="utf-8") as file:
file.write("Another note\n")15.4 Reading line by line
For large files, process them one line at a time:
with open("notes.txt", "r", encoding="utf-8") as file:
for line in file:
print(line.rstrip())This avoids loading the entire file into memory at once.
15.5 File paths
Use pathlib for clearer path manipulation:
from pathlib import Path
path = Path("data") / "articles.txt"
print(path.exists())
print(path.name)
print(path.suffix)Create directories:
Path("data").mkdir(parents=True, exist_ok=True)A relative path is interpreted in relation to the program's current working directory, which is not necessarily the same as the directory containing the Python file. This is a frequent cause of file-not-found errors.
15.6 JSON files
JSON is a text format commonly used to store and exchange structured data.
import json
article = {
"title": "Learning Python",
"published": True,
"views": 100,
}Write the data:
with open("article.json", "w", encoding="utf-8") as file:
json.dump(article, file, indent=2)Read it back:
with open("article.json", "r", encoding="utf-8") as file:
loaded_article = json.load(file)
print(loaded_article["title"])JSON supports objects, arrays, strings, numbers, Booleans, and null-like values. Python's JSON library maps these to appropriate Python data types, but not every Python object can be serialized directly.
15.7 CSV files
CSV is commonly used for tabular data.
import csv
with open("people.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(["name", "age"])
writer.writerow(["Alex", 25])
writer.writerow(["Sam", 30])Read the data:
with open("people.csv", "r", newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
for row in reader:
print(row["name"], row["age"])Use the csv module rather than manually splitting every line on commas. Real CSV data can contain quoted fields, commas inside values, and embedded line breaks.
Exercises: Files
Exercise 1: Create a file containing five lines and read them back.
Exercise 2: Write a program that counts the lines in a text file.
Exercise 3: Store a small list of tasks in a JSON file and load them on the next run.
Exercise 4: Read a CSV file and calculate the total of a numeric column.
Exercise 5: Attempt to open a nonexistent file and handle the failure in a user-friendly way.
Part 16: Modules, Packages, and Project Structure
16.1 What is a module?
A module is a Python file that can be imported by another file.
Suppose you have math_helpers.py:
def add(a, b):
return a + b
def multiply(a, b):
return a * bAnother file can import these functions:
from math_helpers import add, multiply
print(add(2, 3))
print(multiply(4, 5))Modules help divide a large application into understandable pieces.
16.2 Importing modules
Import an entire module:
import math
print(math.sqrt(25))Import specific functionality:
from pathlib import PathUse aliases when helpful:
import statistics as stats
print(stats.mean([10, 20, 30]))Avoid importing everything indiscriminately with from module import *. It can make it unclear where a name comes from and can introduce naming conflicts.
16.3 The __name__ convention
A Python file can be executed directly or imported by another module.
This common pattern allows a file to define reusable functions while also providing a direct entry point:
def main():
print("Program started")
if __name__ == "__main__":
main()When the file is run directly, its __name__ is set to "__main__".
When imported as a module, its __name__ is normally the module's name.
This pattern is useful for command-line applications, scripts, and reusable modules.
16.4 What is a package?
A package groups related modules into a structured directory.
For example:
article_app/
__init__.py
models.py
services.py
storage.pyThe files can have separate responsibilities. models.py might define data structures, services.py might implement business rules, and storage.py might handle persistence.
Python also supports namespace packages, so __init__.py is not required in every possible package layout. For a simple beginner project, including it in regular packages is a reasonable starting convention.
16.5 Avoid circular imports
A circular import occurs when module A imports module B while module B imports module A, creating a dependency cycle.
Circular imports can result in partially initialized modules and confusing errors.
Ways to reduce the risk include:
Separating shared code into a third module.
Keeping dependencies directed from lower-level utilities toward higher-level application logic.
Avoiding unnecessary work during module import.
Moving imports into functions only when there is a clear reason to do so.
Exercises: Modules
Exercise 1: Create one file containing arithmetic functions and import them into another file.
Exercise 2: Create a main() function and use the __name__ guard.
Exercise 3: Split a small calculator into separate modules for calculations and user interaction.
Exercise 4: Draw a dependency diagram for your project. Make sure you can explain which modules depend on which other modules.
Part 17: Object-Oriented Programming
Object-oriented programming, or OOP, organizes software around objects that combine data and behavior.
Python supports OOP, but you do not need to use classes for every problem. Functions and simple data structures are often sufficient.
17.1 Classes and objects
A class defines a kind of object. An instance is a particular object created from that class.
class User:
pass
user = User()The class User defines a type. The statement User() creates an instance of it.
17.2 Attributes and __init__
class User:
def __init__(self, name, age):
self.name = name
self.age = age
user = User("Alex", 25)
print(user.name)
print(user.age)__init__() initializes the new instance's attributes.
self is the conventional name for the instance on which an instance method is operating. Python supplies the instance automatically when the method is called through that instance.
17.3 Methods
Methods are functions defined inside classes.
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}!"
user = User("Alex")
print(user.greet())The call user.greet() passes user as the method's instance argument.
17.4 Encapsulation
Encapsulation means organizing data and the operations that work on it into a coherent interface.
Python does not normally enforce private instance attributes in the same way as some other languages. A leading underscore, such as _balance, signals that an attribute is intended for internal use.
Double-leading-underscore names trigger name mangling, which helps avoid certain naming collisions in class hierarchies. It is not a security mechanism.
17.5 Inheritance
Inheritance lets a class extend another class.
class Animal:
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self):
return "Woof"
dog = Dog()
print(dog.speak())Dog inherits behavior from Animal and overrides speak().
Use inheritance when a genuine subtype relationship makes the design clearer. Do not use it simply to reuse a few lines of code.
17.6 Composition
Composition builds a larger object by combining smaller objects.
For many applications, composition is easier to maintain than deep inheritance hierarchies.
For example, an ArticleService could receive an object responsible for storing articles. This separates business logic from storage and makes it easier to test or replace the storage implementation.
17.7 Class variables and instance variables
An instance variable belongs to a particular instance. A class variable is shared through the class.
class Article:
category = "Technology"
def __init__(self, title):
self.title = title
first = Article("Python Basics")
second = Article("Working With Files")Both instances can access category. Each has its own title.
Be careful when a class variable is a mutable collection: modifying it can affect all instances that share it.
17.8 Properties
Properties allow methods to be accessed through attribute-like syntax.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
rectangle = Rectangle(10, 5)
print(rectangle.area)Here, area behaves like a calculated attribute.
17.9 Dataclasses
Dataclasses reduce boilerplate for classes that primarily store data.
from dataclasses import dataclass
@dataclass
class Article:
title: str
author: str
published: bool = False
article = Article("Python Guide", "Alex")
print(article)Python generates common methods such as an initializer and a useful representation.
Dataclasses are often useful for structured records. They do not automatically validate every value at runtime.
Exercises: OOP
Exercise 1: Create a Book class with title, author, and publication year.
Exercise 2: Add a method that returns a formatted description of the book.
Exercise 3: Create a Student class with a method that calculates the average of a list of scores.
Exercise 4: Create a base Notification class and two specialized classes for different notification types. Consider whether inheritance actually improves the design.
Exercise 5: Represent a simple data record using a dataclass instead of a regular class. Compare the amount of code required.
Part 18: Iterators and Generators
These concepts explain how Python processes sequences of values, including sequences that are too large to store entirely in memory.
18.1 Iterables and iterators
An iterable is an object that can provide its items one at a time.
A list is iterable:
numbers = [10, 20, 30]
for number in numbers:
print(number)An iterator is an object that produces values one at a time when requested.
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))Calling next() after the iterator is exhausted normally raises StopIteration.
Most developers do not need to manually create iterators for ordinary loops, but understanding the mechanism explains how Python processes collections.
18.2 Generators
A generator produces values lazily rather than building and returning the entire collection at once.
def count_up_to(limit):
number = 1
while number <= limit:
yield number
number += 1
for value in count_up_to(5):
print(value)The yield statement produces a value and suspends the generator. When iteration resumes, execution continues from that point.
This is useful for large files, data streams, and sequences whose values are expensive to calculate.
18.3 Generator expressions
A generator expression resembles a list comprehension but uses parentheses.
squares = (number ** 2 for number in range(1, 1000000))This does not immediately build a million-element list. Values are calculated as they are requested.
for square in squares:
if square > 100:
break
print(square)Lazy evaluation can save memory, but it also means the generator is consumed as it is iterated. A generator is not automatically reusable like a list.
Exercises: Iterators and generators
Exercise 1: Write a generator that produces numbers from 1 to a given limit.
Exercise 2: Write a generator that yields only even numbers.
Exercise 3: Compare the memory and behavior of a list comprehension with a generator expression over a large range.
Exercise 4: Create a generator that reads a large text file one line at a time and yields only lines containing a chosen word.
Part 19: Decorators and Context Managers
These features appear frequently in libraries and more advanced Python code.
19.1 Decorators
A decorator takes a function or another callable and returns a replacement or wrapper.
def announce(function):
def wrapper():
print("Starting")
function()
print("Finished")
return wrapper
@announce
def greet():
print("Hello")
greet()The @announce syntax applies the decorator to greet. Conceptually, the name is rebound to the callable returned by the decorator.
Decorators are used for logging, timing, caching, authorization checks, and framework features.
For general-purpose decorators, functools.wraps preserves useful metadata from the wrapped function:
from functools import wraps
def announce(function):
@wraps(function)
def wrapper(*args, **kwargs):
print("Starting")
return function(*args, **kwargs)
return wrapperA good wrapper passes arguments through and preserves the wrapped function's return value.
19.2 Context managers
The with statement controls the lifetime of a resource.
You have already seen it with files:
with open("notes.txt", encoding="utf-8") as file:
contents = file.read()The context manager arranges for cleanup when the block exits.
Other examples include managing locks, database transactions, temporary changes to state, and certain network or system resources.
19.3 Creating a context manager
The contextlib module can create a context manager from a generator:
from contextlib import contextmanager
@contextmanager
def managed_operation():
print("Start")
try:
yield
finally:
print("Cleanup")
with managed_operation():
print("Working")Output:
Start
Working
CleanupCleanup runs when the context is exited, including when an exception leaves the block.
These features are useful when learning how libraries manage resources safely.
Exercises: Decorators and context managers
Exercise 1: Write a decorator that prints the name of the function before calling it.
Exercise 2: Modify the decorator so that it supports arguments and returns the wrapped function's result.
Exercise 3: Use with to write and then read a file.
Exercise 4: Create a small context manager that prints a start message and a cleanup message, then raise an exception inside its block to observe what happens.
Part 20: Type Hints
Python is dynamically typed, but it supports optional type annotations.
Type hints describe the kinds of values a function expects or returns. They help editors, static analyzers, and developers understand the intended interface.
20.1 Basic annotations
def add(a: int, b: int) -> int:
return a + bThe annotations indicate that a, b, and the return value are intended to be integers.
Python generally does not enforce these annotations automatically at runtime.
This code may still run:
add("hello", "world")The function returns the combined string. A static type checker would normally flag the call because it conflicts with the annotations.
20.2 Collections and optional values
Modern Python supports built-in collection annotations:
def average(scores: list[float]) -> float:
return sum(scores) / len(scores)This function needs additional validation if the list can be empty.
A function that may return no result can be annotated as:
def find_name() -> str | None:
return NoneThe | union syntax requires Python 3.10 or newer.
20.3 Why type hints help
Type hints can:
Make function contracts clearer.
Improve editor autocomplete.
Help identify mistakes before execution.
Make refactoring safer.
Help large teams agree on data structures.
They are especially helpful as applications grow.
However, type hints do not replace runtime validation of untrusted input. Data received from an API or a user must still be checked.
Exercises: Type hints
Exercise 1: Add parameter and return annotations to a calculator.
Exercise 2: Write a function that takes a list of integers and returns a list of their squares.
Exercise 3: Write a function that returns a string or None, and explain what each outcome means.
Exercise 4: Deliberately call an annotated function with the wrong type. Run it and then try a static type checker to understand the difference between runtime behavior and static analysis.
Part 21: Python's Standard Library
The standard library comes with Python and provides many capabilities without installing third-party packages.
21.1 pathlib
Work with filesystem paths:
from pathlib import Path
path = Path("data") / "report.txt"
print(path.name)
print(path.suffix)
print(path.exists())21.2 datetime
Work with dates and times:
from datetime import datetime, date
today = date.today()
now = datetime.now()
print(today)
print(now)For systems that exchange timestamps across locations, learn about time zones and timezone-aware datetimes. An unqualified local time is not always sufficient to identify a unique instant.
21.3 collections
The collections module includes useful data structures.
Count items with Counter:
from collections import Counter
words = ["python", "web", "python", "data"]
counts = Counter(words)
print(counts)Use defaultdict when a missing key should automatically receive a default value, or deque for efficient appending and removal at both ends.
21.4 itertools
The itertools module supplies efficient tools for combining and iterating over data.
For example:
from itertools import product
for pair in product([1, 2], ["a", "b"]):
print(pair)It produces every combination of one value from each input iterable.
Some itertools functions are lazy and can process large sequences without materializing all results in memory.
21.5 functools
The functools module provides tools such as lru_cache, partial, and wraps.
Memoization with lru_cache:
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(number):
if number < 2:
return number
return fibonacci(number - 1) + fibonacci(number - 2)Caching avoids recomputing results for repeated inputs. It is most appropriate when the function is deterministic and its inputs are suitable cache keys. Consider memory use and invalidation requirements in long-running applications.
21.6 argparse
argparse helps build command-line interfaces.
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--name", default="Guest")
args = parser.parse_args()
print(f"Hello, {args.name}!")Run the program with:
python greet.py --name AlexA command-line interface is a practical way to turn a useful script into a reusable tool.
21.7 Logging
Use the logging module rather than relying only on print() in larger programs.
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Application started")
logging.warning("A recoverable issue occurred")
logging.error("An operation failed")Logging supports severity levels and can be configured to write to files or other destinations. Production applications should use consistent, useful logging without exposing secrets or unnecessary sensitive data.
Exercises: Standard library
Exercise 1: Write a script that lists all filenames in a directory using pathlib.
Exercise 2: Count words in a sentence using Counter.
Exercise 3: Create a command-line script that accepts a filename as an argument.
Exercise 4: Replace important print() statements in a small application with appropriate logging calls.
Part 22: Virtual Environments, Packages, and Dependencies
Python includes a standard library, but real projects often require external packages.
The challenge is ensuring that one project can use its dependencies without disrupting another project.
22.1 What is a package?
A package is reusable software that can be installed or included in a project.
Examples include libraries for:
Web requests.
Web application frameworks.
Data processing.
Testing.
Database connectivity.
Machine learning.
The Python Package Index, or PyPI, is a major source of third-party Python packages.
22.2 What is pip?
pip installs Python packages.
python -m pip install requestsUsing python -m pip helps make clear which Python interpreter is associated with the installer command.
Installing a package globally may create conflicts between projects. This is why virtual environments are important.
22.3 What is a virtual environment?
A virtual environment provides an isolated package-installation environment for a project.
Imagine two projects:
Project A needs one version of a library.
Project B needs another version.
Installing every dependency globally can create conflicts. Virtual environments keep project dependencies separate.
Create one:
python -m venv .venvOn Windows, activate it from Command Prompt with:
.venv\Scripts\activateOn macOS or Linux, activate it with a compatible shell:
source .venv/bin/activateThen install packages inside the environment:
python -m pip install requestsWhen you finish:
deactivateThe .venv directory should normally be excluded from version control. The environment can generally be recreated from your project configuration, so it should not need to be committed.
22.4 Managing dependencies
For a simple project, you may record installed packages with:
python -m pip freeze > requirements.txtRecreate an environment's installed package set with:
python -m pip install -r requirements.txtFor larger projects, modern Python packaging commonly uses pyproject.toml to define project metadata, dependencies, and build configuration.
Do not confuse the name of an import package with the name of the distribution package installed by pip. Some libraries use different names for installation and import.
22.5 Understand your environment
When a package appears installed but Python cannot import it, the most common issue is that the package was installed into a different interpreter or virtual environment than the one running the program.
Check which Python executable you are using and whether your environment is activated.
This is a practical debugging skill that will save significant time.
Exercises: Environments and packages
Exercise 1: Create a virtual environment for a new project.
Exercise 2: Install one third-party package into that environment and import it.
Exercise 3: Record the project's dependencies and recreate them in a fresh environment.
Exercise 4: Create two environments and verify that a package installed into one is not automatically available in the other.
Part 23: Testing Python Programs
Testing verifies that your code behaves as intended. It also makes later changes safer.
23.1 Why testing matters
Suppose you write a function to calculate discounts. It works for a price of 100 and a discount of 10%.
That does not prove it handles zero, invalid percentages, negative values, or non-numeric input.
Tests make expected behavior explicit.
23.2 Assertions
Python's assert statement checks a condition and raises AssertionError when it is false.
def add(a, b):
return a + b
assert add(2, 3) == 5
assert add(-1, 1) == 0Assertions are useful in tests and for checking internal assumptions during development. Do not use them as the sole validation mechanism for untrusted input because optimized execution can disable assertions.
23.3 Unit tests
A unit test focuses on one small piece of behavior.
Python's built-in unittest framework provides test classes and assertion methods.
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_adds_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_adds_negative_number(self):
self.assertEqual(add(-1, 1), 0)
if __name__ == "__main__":
unittest.main()Save this as a Python file and run it. The framework discovers the test methods and reports which tests passed or failed.
External tools such as pytest provide another widely used testing approach.
23.4 What should you test?
Test:
Ordinary valid input.
Boundary cases.
Invalid input.
Empty collections.
Missing files or data.
Errors from external dependencies.
The behavior that previously caused a bug.
A good test verifies an expected behavior without unnecessarily duplicating the implementation.
23.5 Integration tests
A unit test might test a function that calculates a total. An integration test could verify that an application reads data from a file, transforms it, and writes the expected output.
As projects grow, integration tests reveal problems that isolated unit tests cannot catch.
Exercises: Testing
Exercise 1: Write tests for a function that converts Celsius to Fahrenheit.
Exercise 2: Test a function that finds the largest number in a list, including an empty-list case.
Exercise 3: Create a test for a file-processing function using a temporary file.
Exercise 4: Introduce a small bug into a function and confirm that a test catches it.
Part 24: APIs and the Internet
Python can communicate with other applications over a network. This is the foundation of API integrations, automation tools, and many data applications.
24.1 What is an API?
An API is an interface through which one software system requests functionality or data from another.
For example, an application might request weather information from a remote service.
The general flow is:
Your Python program constructs a request.
The request is sent over a network.
The remote server processes it.
The server returns a response.
Your program examines the response and uses its data.
A common protocol for web APIs is HTTP.
24.2 JSON API responses
A server may respond with JSON:
{
"name": "Alex",
"role": "Developer"
}Python's json module can parse JSON text into Python objects.
import json
text = '{"name": "Alex", "role": "Developer"}'
data = json.loads(text)
print(data["name"])In this example, json.loads() parses a string. json.load() reads JSON from a file-like object.
24.3 Making an HTTP request
Python's standard library includes urllib, so you can make basic HTTP requests without installing an external dependency.
import json
from urllib.request import urlopen
url = "https://example.com/data.json"
with urlopen(url, timeout=10) as response:
data = json.load(response)
print(data)This is an illustrative request. It only works if the URL provides a valid JSON response and the service is reachable.
Real API clients should account for network errors, timeouts, HTTP error responses, authentication requirements, rate limits, and changes in response formats.
Libraries such as requests can make common HTTP operations more convenient.
24.4 HTTP methods
Common HTTP methods include:
GET— retrieve information.POST— submit data or create a resource.PUT— replace a resource representation.PATCH— partially update a resource.DELETE— request deletion.
A response also includes a status code, which helps indicate whether the request succeeded or encountered an issue.
For example, 200 generally means success, 201 often indicates creation, 404 means the requested resource was not found, and 500 indicates an internal server error.
Do not assume every HTTP response represents success merely because a connection was established.
24.5 API reliability
When building API integrations:
Set timeouts.
Handle expected network and HTTP errors.
Validate the received data.
Avoid logging credentials or sensitive information.
Respect rate limits.
Use retries selectively.
Avoid retrying non-idempotent operations without considering duplicate effects.
Exercises: APIs
Exercise 1: Parse a JSON string into a dictionary.
Exercise 2: Use a public test API to retrieve JSON and print selected fields.
Exercise 3: Add a timeout and handle likely network failures.
Exercise 4: Write a function that accepts API data and checks whether required fields are present before using them.
Part 25: Databases With Python
Files are useful, but applications often need structured, searchable, persistent data. A database helps organize that information and supports queries that would be cumbersome to implement manually.
25.1 What is a database?
A database stores data in a form that applications can retrieve and modify.
A relational database organizes information into tables. A table has columns and rows.
For example, an articles table might contain:
id | title | published |
|---|---|---|
1 | Python Basics | True |
2 | Working With Files | False |
Each row represents a record. Each column represents a field.
SQL is a language used to define, query, and modify relational data.
25.2 SQLite
Python includes the sqlite3 module, which allows you to work with SQLite databases without running a separate database server.
Create a connection and table:
import sqlite3
connection = sqlite3.connect("articles.db")
connection.execute("""
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
published INTEGER NOT NULL DEFAULT 0
)
""")
connection.commit()
connection.close()The database file is stored on disk, so its data remains after the program ends.
25.3 Insert and query data
import sqlite3
with sqlite3.connect("articles.db") as connection:
connection.execute(
"INSERT INTO articles (title, published) VALUES (?, ?)",
("Learning Python", 0),
)
rows = connection.execute(
"SELECT id, title, published FROM articles"
).fetchall()
for row in rows:
print(row)SQLite uses ? placeholders for values in this example.
The connection context manager commits a successful transaction and rolls back the transaction if an exception escapes the block. It does not close the connection itself, which is why resource lifetime still deserves attention in larger programs.
25.4 Why parameterized queries matter
Never construct SQL by inserting untrusted strings directly into a query.
Unsafe:
query = f"SELECT * FROM users WHERE name = '{name}'"Safe:
cursor = connection.execute(
"SELECT * FROM users WHERE name = ?",
(name,),
)Parameterized queries separate the SQL command from the data, helping prevent SQL injection.
25.5 Transactions and data integrity
A transaction groups related database operations into a unit that can be committed or rolled back.
This is important when multiple changes must succeed together. For example, transferring money between accounts should not update one account successfully while leaving the other unchanged.
Learn transactions, constraints, indexes, foreign keys, and how to handle concurrent modifications before building applications that manage important persistent state.
Exercises: Databases
Exercise 1: Create a database with a table of books.
Exercise 2: Insert several rows and query them.
Exercise 3: Update a row using a parameterized query.
Exercise 4: Delete a row and confirm the record is gone.
Exercise 5: Build a small command-line application that lets a user add and list records stored in SQLite.
Part 26: Concurrency and Asynchronous Programming
Some programs spend a large amount of time waiting for files, network requests, databases, or other external operations. Python provides several approaches for managing such workloads.
These approaches are related but solve different problems.
26.1 Sequential execution
def first():
print("First")
def second():
print("Second")
first()
second()The second function runs after the first returns.
This simple model is sufficient for many programs.
26.2 Threading
Threads can be useful when tasks wait for I/O, such as network responses or file operations.
from concurrent.futures import ThreadPoolExecutor
import time
def work(number):
time.sleep(1)
return number * 2
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(work, [1, 2, 3]))
print(results)In suitable workloads, threading lets the program make progress on other tasks while one task is waiting.
Threads share memory, which means they can also introduce race conditions if several operations access or modify shared state without suitable synchronization.
26.3 Multiprocessing
Multiprocessing runs work in separate processes. It is often suitable for CPU-heavy work that can be divided into independent tasks.
from concurrent.futures import ProcessPoolExecutor
def square(number):
return number * number
if __name__ == "__main__":
with ProcessPoolExecutor() as executor:
results = list(executor.map(square, range(10)))
print(results)The if __name__ == "__main__": guard is especially important in programs that create processes on platforms that start worker processes by importing the main module.
Processes have additional startup and communication overhead. Use them when their benefits justify that cost.
26.4 Asyncio
asyncio supports cooperative asynchronous programming, especially for I/O-bound tasks that can use asynchronous APIs.
import asyncio
async def main():
print("Starting")
await asyncio.sleep(1)
print("Finished")
asyncio.run(main())An async def function creates a coroutine function. Calling it produces a coroutine object; it does not immediately execute the function in the same way as a normal function call.
await suspends the coroutine while the awaited operation is pending, allowing the event loop to run other eligible work.
For a single sleep, asyncio offers no dramatic benefit. It becomes valuable when coordinating many concurrent asynchronous operations.
26.5 Choosing the right approach
Workload | Common approach |
|---|---|
Simple, sequential tasks | Ordinary functions |
Several I/O-bound blocking operations | Threads, where appropriate |
CPU-intensive independent tasks | Processes or other parallel-computing approaches |
Many compatible asynchronous network operations |
|
These are general guidelines, not absolute rules. The right choice depends on the workload, libraries, runtime, and communication costs.
The Global Interpreter Lock (GIL) in conventional CPython builds limits how multiple threads execute Python bytecode simultaneously. Python also has optional free-threaded builds in recent releases, but their availability and compatibility depend on the selected build and libraries. Do not assume every Python installation behaves identically.
Exercises: Concurrency
Exercise 1: Run several delayed functions sequentially and compare the elapsed time with a thread pool.
Exercise 2: Write an async function that performs several simulated I/O waits and compare sequential awaits with asyncio.gather().
Exercise 3: Use a process pool to calculate squares for a collection of integers.
Exercise 4: Explain why concurrency can improve a network-heavy program without making a purely sequential mathematical algorithm faster.
Part 27: Performance and Algorithmic Thinking
Python code can be correct but still too slow for a particular workload. Improving performance begins with understanding the work being done.
27.1 Algorithmic complexity
Big O notation describes how the resource requirements of an algorithm grow as its input grows.
Complexity | Example |
|---|---|
| Accessing a list element by index |
| Binary search in sorted data |
| Scanning every element once |
| Typical comparison-based sorting |
| Comparing every pair using nested loops |
These describe growth patterns, not exact timings.
For example, searching through a list of ten items may be trivial even if the algorithm is O(n). The same approach may be too expensive for millions of items.
27.2 Lists versus sets
Suppose you repeatedly need to check whether an identifier appears in a collection.
allowed_ids = [10, 20, 30, 40]Membership checks in a list require searching through elements in the general case.
A set is often more suitable for repeated membership checks:
allowed_ids = {10, 20, 30, 40}
print(20 in allowed_ids)Set membership is typically close to constant time on average, although worst-case behavior depends on hashing and implementation details.
The right data structure can be more important than micro-optimizing a line of code.
27.3 Profile before optimizing
Do not rewrite working code simply because you think a particular expression must be slow.
Measure it first.
from time import perf_counter
start = perf_counter()
total = sum(range(1_000_000))
elapsed = perf_counter() - start
print(total)
print(f"Elapsed: {elapsed:.4f} seconds")For more detailed investigations, use profiling tools that report where the program spends its time.
27.4 Memory efficiency
Loading a very large file into memory may be unnecessary:
with open("large.txt", encoding="utf-8") as file:
for line in file:
process(line)Processing one line at a time can reduce memory requirements.
Generators, iterators, and streaming APIs are useful for similar reasons.
27.5 Caching
Caching stores the results of selected operations so they do not need to be repeated.
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_calculation(number):
# Illustrative calculation.
return number ** 2Caching is useful when repeated calls have the same inputs and the result remains valid.
It can also consume memory, return stale values, or add unnecessary complexity. Consider cache lifetime, invalidation, and whether the function is safe to cache.
Exercises: Performance
Exercise 1: Compare membership checks using a list and a set with a large collection.
Exercise 2: Write two versions of a program that process a large text file: one that reads the whole file and another that processes it line by line.
Exercise 3: Use a timer to compare two algorithms that produce the same output.
Exercise 4: Identify whether a slow operation is caused mainly by computation, disk access, or waiting for an external service before selecting an optimization.
Part 28: Security and Reliable Application Design
Good Python developers consider security and reliability from the beginning.
28.1 Validate untrusted input
User input, request bodies, files, environment variables, and API responses should not be assumed correct.
Validate the type, format, length, and meaning of incoming data before using it.
Validation should reflect actual business rules, not just whether the input can be converted to a particular type.
28.2 Protect against SQL injection
Use parameterized queries when interacting with a database.
Do not create SQL statements by concatenating user input directly into the query.
28.3 Be careful with shell commands
Python can run external processes, but interpolating untrusted input into a shell command is dangerous.
Prefer structured argument lists with subprocess.run() and avoid shell parsing when it is unnecessary.
import subprocess
result = subprocess.run(
["python", "--version"],
check=True,
capture_output=True,
text=True,
)
print(result.stdout)The example runs a fixed command. If arguments come from untrusted users, validate them and understand the risks associated with the program being invoked.
28.4 Never treat pickle as safe untrusted input
Python's pickle module can reconstruct Python objects during deserialization. Loading a malicious pickle can execute arbitrary code.
Do not unpickle untrusted or tampered data.
For cross-system data exchange, JSON is often a safer choice because its data model is more limited, though it still requires appropriate validation.
28.5 Manage secrets properly
Do not hardcode passwords, private API keys, database credentials, or signing secrets into source code.
Use environment-specific configuration and secret-management facilities appropriate to your environment.
Avoid logging sensitive credentials or personal data unnecessarily.
28.6 Handle failures deliberately
A reliable application distinguishes between expected failures and unexpected bugs.
For example:
Invalid user input should produce a clear validation error.
A missing optional file might lead to a default behavior.
A database outage might require retrying or returning a service error.
An unexpected programming error should be logged and investigated.
Catching every exception and silently continuing can hide defects and produce corrupted or misleading results.
Exercises: Security and reliability
Exercise 1: Review a script that accepts user input and identify every place where input should be validated.
Exercise 2: Replace a string-constructed SQL query with a parameterized query.
Exercise 3: Read documentation for subprocess.run() and explain why structured argument lists are preferable to blindly passing a shell command.
Exercise 4: Create a program that reads configuration from environment variables rather than embedding a secret directly in the source.
Part 29: Build a Complete Project : A Python Article Archive
You have now encountered enough core concepts to build a small application that combines them.
The project will be a command-line article archive. It will let a user add, list, search, and delete records. It will persist data in JSON first and can later be upgraded to use SQLite.
29.1 Define an article
Start with a dictionary:
article = {
"id": 1,
"title": "Learning Python",
"category": "Programming",
"published": False,
}Decide which fields are required and what makes two articles distinct.
29.2 Separate the program into responsibilities
A sensible small-project structure is:
article_archive/
main.py
models.py
storage.py
services.py
tests/
test_services.py
data/
articles.jsonEach file has a purpose:
main.pyhandles the command-line interaction.models.pydefines the article data structure.storage.pyreads and writes persisted data.services.pyimplements operations such as searching and validating.tests/contains automated tests.
This is an example structure, not a requirement. Start simpler if that helps you understand the program.
29.3 Load and save JSON
import json
from pathlib import Path
DATA_FILE = Path("articles.json")
def load_articles():
if not DATA_FILE.exists():
return []
with DATA_FILE.open(encoding="utf-8") as file:
return json.load(file)
def save_articles(articles):
with DATA_FILE.open("w", encoding="utf-8") as file:
json.dump(articles, file, indent=2)This is a starting point. Production code should account for malformed JSON, write failures, path selection, and possible data-loss risks when overwriting files.
29.4 Add article validation
def validate_article(title, category):
if not isinstance(title, str) or not title.strip():
raise ValueError("Title is required")
if not isinstance(category, str) or not category.strip():
raise ValueError("Category is required")
return {
"title": title.strip(),
"category": category.strip(),
"published": False,
}Validation separates input requirements from the code that stores the article.
29.5 Implement search
def search_articles(articles, query):
query = query.strip().casefold()
return [
article
for article in articles
if query in article["title"].casefold()
]This implements case-insensitive substring matching on the title. It does not search categories or content; those can be added as later requirements.
29.6 Build the interface
Create a loop that displays a menu and responds to the user's choice.
def main():
while True:
print("\nArticle Archive")
print("1. List articles")
print("2. Search articles")
print("3. Exit")
choice = input("Choose an option: ").strip()
if choice == "1":
print("List operation goes here")
elif choice == "2":
print("Search operation goes here")
elif choice == "3":
break
else:
print("Invalid option")
if __name__ == "__main__":
main()Replace the placeholder messages with real operations, then add creation and deletion.
29.7 Add tests
Test the logic independently from the menu.
For example, verify that:
A valid article can be created.
A missing title is rejected.
Search is case-insensitive.
Searching for an unknown title returns an empty list.
Saved records can be loaded again.
Do not wait until the project is finished to add tests.
29.8 Upgrade it
Once the JSON version works, introduce additional capabilities in stages:
Replace JSON storage with SQLite.
Add article IDs and unique constraints.
Support editing records.
Add filtering by category and publication status.
Validate all user input.
Add automated tests.
Configure logging.
Create a proper package structure.
Add a command-line interface using
argparse.Measure performance if the dataset grows.
This project provides a practical bridge from individual Python syntax features to application design.
Part 30: Common Mistakes Beginners Make
Understanding common mistakes is part of becoming a reliable developer.
Mistake 1: Confusing assignment and equality
name = "Alex" # Assignment
name == "Alex" # ComparisonThey perform different operations.
Mistake 2: Forgetting indentation
Python uses indentation to define blocks. A statement placed at the wrong indentation level can change behavior or cause an error.
Mistake 3: Forgetting that input() returns a string
Convert user input explicitly before performing numeric calculations.
Mistake 4: Returning nothing from a function that needs a result
A function without a return statement returns None.
Printing a value does not automatically return it.
Mistake 5: Modifying a list while iterating over it
Removing or inserting elements during iteration can skip items or produce unexpected behavior.
When filtering a list, creating a new list is often safer:
numbers = [1, 2, 3, 4, 5]
even_numbers = [
number for number in numbers
if number % 2 == 0
]Mistake 6: Using mutable default arguments
Do not use a single shared list or dictionary as a default parameter unless sharing that state is explicitly intended.
Mistake 7: Catching every exception
A broad except Exception can hide unexpected defects. Catch specific exceptions where you can handle them meaningfully.
Mistake 8: Assuming a file path is relative to the script
Relative paths are normally interpreted relative to the current working directory. Inspect the actual path when a file cannot be found.
Mistake 9: Installing packages into the wrong environment
Use a virtual environment and check which interpreter runs your code.
Mistake 10: Building one enormous file
Split code by responsibility as a project grows. Avoid both extremes: one giant file and an unnecessarily complex architecture.
Mistake 11: Writing code without tests
A program that worked once is not necessarily correct. Test normal cases, edge cases, and expected failures.
Mistake 12: Optimizing before measuring
Choose data structures appropriately, then profile real bottlenecks before making complexity-inducing changes.
Mistake 13: Trusting every piece of external data
API responses, uploaded files, command-line arguments, and user input should be validated before they are relied upon.
Part 31: A Structured Roadmap From Beginner to Advanced
You do not need to learn everything at once. The following progression builds knowledge in an order that makes later concepts easier to understand.
Stage 1 : Programming fundamentals
Learn:
Variables and types.
Operators and expressions.
Input and output.
Conditions and loops.
Basic functions.
Build a calculator, number-guessing game, unit converter, and simple quiz.
Move forward when: you can take a small problem, break it into steps, and implement it without copying a complete solution.
Stage 2 : Data structures and problem-solving
Learn:
Strings.
Lists, tuples, sets, and dictionaries.
Iteration and comprehensions.
References and mutability.
Functions with parameters and return values.
Build a contact book, word counter, shopping-cart calculator, and small text-processing utility.
Move forward when: you can choose an appropriate data structure and explain why it fits the task.
Stage 3 : Reliable programs
Learn:
Exceptions and input validation.
Files and paths.
JSON and CSV.
Modules and package structure.
Logging and debugging.
Build a file organizer, report generator, and persistent task manager.
Move forward when: your programs recover gracefully from common errors and separate logic from input and output.
Stage 4 : Structured application development
Learn:
Classes and dataclasses.
Type hints.
Iterators and generators.
Decorators and context managers.
Unit and integration testing.
Build a reusable utility package and a tested command-line application.
Move forward when: you can structure a multi-file project and explain how its components interact.
Stage 5 : Real-world systems
Learn:
HTTP and APIs.
SQLite and SQL.
Virtual environments and dependencies.
Configuration management.
Security and validation.
Build an application that retrieves information from an API and stores it in a database.
Move forward when: your application handles failures, persists data correctly, and can be reproduced in a fresh environment.
Stage 6 : Advanced engineering
Learn:
Concurrency and asynchronous programming.
Algorithmic complexity.
Profiling and performance.
Packaging and deployment.
Architectural trade-offs.
More advanced typing and testing practices.
Build, test, deploy, and maintain a complete project. Read existing code, diagnose real bugs, and improve the application without breaking existing behavior.
Part 32: Final Self-Assessment
Use these questions to test whether you understand the material rather than merely recognize the terminology.
Language fundamentals
Can you explain what happens when Python executes an assignment?
Can you distinguish a string from an integer and convert input safely?
Can you explain the difference between
==andis?Can you use conditions and loops to solve a problem?
Can you write functions with appropriate parameters and return values?
Data and program structure
Can you choose among lists, tuples, sets, and dictionaries?
Can you explain mutable objects and shallow copies?
Can you divide a program into useful modules?
Can you use files and JSON to preserve data?
Can you write and run automated tests?
Practical application development
Can you build a command-line application from a written requirement?
Can you fetch data from an API and handle failures?
Can you persist information in a database?
Can you use a virtual environment and manage dependencies?
Can you debug a traceback and reproduce a bug?
Can you identify common input-validation, SQL-injection, and secret-management risks?
Can you measure performance and explain the relevant bottleneck?
Advanced understanding
Can you explain how references and mutability affect program state?
Can you explain when threads, processes, and asynchronous programming are appropriate?
Can you reason about algorithmic complexity?
Can you create a maintainable project structure without unnecessary abstractions?
Can you explain the trade-offs behind a technical decision?
If you can answer these questions and demonstrate the skills in projects, you will have a strong foundation in Python. You will still encounter unfamiliar problems, but you will have the tools to investigate them systematically.