DOCODIVE
Intermediate Free Learning Path

Python Intermediate Guide

Level up your Python with decorators, generators, advanced OOP, databases, APIs, testing, and real-world projects.

4–6 weeks 20 lessons 3 projects 1 capstone Beginner path required
Start Learning
01

Advanced Functions

25 min
What you'll learn
  • Accept any number of arguments with *args
  • Accept keyword arguments with **kwargs
  • Return multiple values from a function

Beginner functions take a fixed number of parameters. Intermediate functions handle flexible input: *args collects positional arguments into a tuple, **kwargs collects keyword arguments into a dict, and Python lets you return multiple values as a tuple.

Python
def describe_person(name, *scores, **details):
    print(name)
    print("Scores:", scores)
    print("Details:", details)
    return len(scores), sum(scores)

count, total = describe_person("Sufyan", 85, 90, 78, city="Karachi", role="dev")
print(count, total)
Output
python
Sufyan Scores: (85, 90, 78) Details: {'city': 'Karachi', 'role': 'dev'} 3 253
💡 Tip: The order matters: normal parameters first, then *args, then **kwargs.
Try it yourself

Write a function that takes a name and any number of marks, then returns the average.

Use *marks, sum(marks) / len(marks), and handle zero marks.
def average(name, *marks):
    if not marks:
        return 0
    return sum(marks) / len(marks)
print(average("Ali", 80, 90, 100))
02

Scope & Closures

25 min
What you'll learn
  • Understand local, enclosing, and global scope
  • Use global and nonlocal
  • Build a closure that remembers state

Scope decides where a variable is visible. A variable inside a function is local. nonlocal modifies a variable in an outer function, and global modifies a module-level variable. A closure is an inner function that remembers variables from its outer function even after that function returns.

Python
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter())
print(counter())
print(counter())
Output
python
1 2 3
🔎 Important: A closure keeps the outer function's state alive. Each call to make_counter() creates a fresh, independent counter.
Try it yourself

Create a counter that increments by 5 each time it is called.

Inside the inner function, use nonlocal count; count += 5.
def make_step_counter(step):
    total = 0
    def step_up():
        nonlocal total
        total += step
        return total
    return step_up
c = make_step_counter(5)
print(c())
print(c())
03

Decorators

30 min
What you'll learn
  • Explain what a decorator is
  • Write a simple decorator with @
  • Pass arguments through with *args and **kwargs

A decorator is a function that wraps another function to add behaviour without changing its code. You apply it with @decorator above the function. Inside, the wrapper calls the original function and can run code before or after it.

Python
import functools

def announce(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}...")
        result = func(*args, **kwargs)
        print("Finished.")
        return result
    return wrapper

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

print(add(3, 4))
Output
python
Calling add... Finished. 7
✓ Best Practice: Always use @functools.wraps(func) inside a decorator so the wrapped function keeps its original name and docstring.
Try it yourself

Write a decorator that prints the return value of a function after it runs.

Inside wrapper, store the result, print(result), then return it.
import functools

def show_result(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print("Result:", result)
        return result
    return wrapper

@show_result
def double(x):
    return x * 2

double(21)
04

functools Tools

25 min
What you'll learn
  • Use lru_cache to speed up repeated calls
  • Use partial to pre-fill function arguments
  • Use reduce to combine values

The functools module provides powerful utilities for functions. lru_cache memoizes results so expensive calls run once. partial freezes some arguments, creating a simpler function. reduce repeatedly applies a function to combine values in a sequence.

Python
from functools import lru_cache, partial, reduce

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(30))

add_five = partial(lambda a, b: a + b, 5)
print(add_five(10))

print(reduce(lambda a, b: a * b, [1, 2, 3, 4]))
Output
python
832040 15 24
💡 Tip: lru_cache is great for recursive functions like fibonacci — without it, fib(30) would be extremely slow.
Try it yourself

Use partial to create a function that multiplies any number by 3.

partial(lambda a, b: a * b, 3).
from functools import partial
triple = partial(lambda a, b: a * b, 3)
print(triple(7))
05

Advanced Collections

25 min
What you'll learn
  • Use defaultdict to avoid key errors
  • Use Counter to count items fast
  • Use deque for fast appends and pops

collections offers specialised containers. defaultdict provides a default value for missing keys. Counter counts hashable items into a dict-like object. deque is a double-ended queue with fast operations on both ends — better than a list for queues.

Python
from collections import defaultdict, Counter, deque

word_count = defaultdict(int)
for word in ["a", "b", "a", "c", "a"]:
    word_count[word] += 1
print(dict(word_count))

print(Counter(["apple", "apple", "banana"]))

d = deque([1, 2, 3])
d.appendleft(0)
d.append(4)
print(d)
Output
python
{'a': 3, 'b': 1, 'c': 1} Counter({'apple': 2, 'banana': 1}) deque([0, 1, 2, 3, 4])
✓ Best Practice: Use Counter for frequency counting instead of writing a manual loop with a dict.
Try it yourself

Create a defaultdict(list) and append two values to the same missing key.

d = defaultdict(list); d['x'].append(1); d['x'].append(2).
from collections import defaultdict
d = defaultdict(list)
d['x'].append(1)
d['x'].append(2)
print(d['x'])
06

Iterators

25 min
What you'll learn
  • Understand iter() and next()
  • Build a custom iterator class
  • Handle StopIteration

An iterator is an object that produces values one at a time. iter() converts an iterable into an iterator. next() returns the next value and raises StopIteration when done. Custom iterators implement __iter__ and __next__.

Python
class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 0:
            raise StopIteration
        value = self.current
        self.current -= 1
        return value

for n in Countdown(3):
    print(n)
Output
python
3 2 1 0
🔎 Important: A for loop automatically calls iter() and next(), and catches StopIteration to end the loop.
Try it yourself

Create an iterator that counts up from 1 to 5.

In __next__, stop when current exceeds 5.
class CountUp:
    def __init__(self):
        self.current = 0
    def __iter__(self):
        return self
    def __next__(self):
        self.current += 1
        if self.current > 5:
            raise StopIteration
        return self.current
for n in CountUp():
    print(n)
07

Generators

30 min
What you'll learn
  • Use yield to create a generator
  • Understand lazy evaluation and memory savings
  • Use yield from and generator expressions

A generator is a function that uses yield instead of return. It produces values lazily — one at a time — so large sequences don't consume memory. yield from delegates to another generator, and generator expressions are like list comprehensions but lazy.

Python
def squares(limit):
    for n in range(1, limit + 1):
        yield n ** 2

for value in squares(5):
    print(value)

print(sum(x for x in range(1, 101)))
Output
python
1 4 9 16 25 5050
✓ Best Practice: Use a generator when reading large files or producing long sequences — it keeps memory usage tiny.
Try it yourself

Write a generator that yields only even numbers up to 10.

Loop and yield n when n % 2 == 0.
def evens(limit):
    for n in range(limit + 1):
        if n % 2 == 0:
            yield n
print(list(evens(10)))
08

Comprehensions Deep Dive

25 min
What you'll learn
  • Write nested comprehensions
  • Add conditions to comprehensions
  • Compare comprehensions with map and filter

Comprehensions build collections in one readable expression. You can nest loops, add if conditions, and choose between list, dict, and set forms. For simple transformations they are clearer than map/filter; for complex logic, a normal loop is better.

Python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [n for row in matrix for n in row]
print(flat)

evens = [n for n in range(10) if n % 2 == 0]
print(evens)

word_lengths = {w: len(w) for w in ["go", "python", "code"]}
print(word_lengths)
Output
python
[1, 2, 3, 4, 5, 6, 7, 8, 9] [0, 2, 4, 6, 8] {'go': 2, 'python': 6, 'code': 4}
💡 Tip: If a comprehension is hard to read, break it into a normal loop — readability beats cleverness.
Try it yourself

Flatten a 2x2 matrix and keep only odd numbers.

Combine two for clauses and an if condition.
matrix = [[1, 2], [3, 4]]
result = [n for row in matrix for n in row if n % 2 == 1]
print(result)
09

Class Methods & Static Methods

25 min
What you'll learn
  • Use @classmethod and cls
  • Use @staticmethod
  • Build factory methods

Instance methods take self. Class methods take cls and can create objects — useful for alternate constructors (factory methods). Static methods take neither self nor cls and behave like plain functions placed inside a class for organisation.

Python
class Employee:
    company = "DocoDive"

    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    @classmethod
    def from_string(cls, text):
        name, salary = text.split("-")
        return cls(name, int(salary))

    @staticmethod
    def is_valid_salary(salary):
        return salary > 0

e = Employee.from_string("Sufyan-50000")
print(e.name, e.salary, e.company)
print(Employee.is_valid_salary(50000))
Output
python
Sufyan 50000 DocoDive True
✓ Best Practice: Use a classmethod factory when you need multiple ways to create objects from different input formats.
Try it yourself

Add a classmethod that creates an Employee from a dict.

Accept a dict and return cls(d['name'], d['salary']).
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data["salary"])
e = Employee.from_dict({"name": "Ali", "salary": 40000})
print(e.name, e.salary)
10

Properties

25 min
What you'll learn
  • Use @property for read-only access
  • Add setters with validation
  • Use deleters

Properties let you control attribute access. @property exposes a method as an attribute. The @setter validates values before assigning. This gives you clean attribute syntax with custom logic behind the scenes.

Python
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(5)
print(c.area)
c.radius = 10
print(c.area)
Output
python
78.53975 314.159
🔎 Important: Use properties to keep a clean public API while hiding validation or computed values inside the class.
Try it yourself

Create a Temperature class where the setter rejects values below -273.

Raise ValueError when value < -273.
class Temperature:
    def __init__(self, c):
        self.celsius = c
    @property
    def celsius(self):
        return self._celsius
    @celsius.setter
    def celsius(self, value):
        if value < -273:
            raise ValueError("Too cold")
        self._celsius = value
t = Temperature(25)
print(t.celsius)
11

Inheritance

30 min
What you'll learn
  • Create subclasses with super()
  • Override parent methods
  • Use isinstance and issubclass

Inheritance lets a class reuse and extend another class. The child class inherits methods and attributes, can override them, and can call the parent with super(). isinstance checks an object's type, issubclass checks class relationships.

Python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

d = Dog("Rex")
c = Cat("Milo")
print(d.speak())
print(c.speak())
print(isinstance(d, Animal), issubclass(Dog, Animal))
Output
python
Rex says Woof! Milo says Meow! True True
✓ Best Practice: Override only the methods that differ. Reuse everything else from the parent — that's the whole point of inheritance.
Try it yourself

Create a Bird subclass whose speak() returns 'tweet' with the name.

Override speak() and use self.name.
class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."
class Bird(Animal):
    def speak(self):
        return f"{self.name} says tweet!"
b = Bird("Kiwi")
print(b.speak())
12

Dunder Methods

30 min
What you'll learn
  • Use __str__ and __repr__
  • Implement __eq__ for equality
  • Implement __len__ and __lt__

Dunder (double underscore) methods control built-in behaviour. __str__ gives a readable string for users, __repr__ gives a developer-friendly representation. __eq__ defines ==, __len__ defines len(), and __lt__ enables sorting with <.

Python
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):
        return f"{self.title}"

    def __repr__(self):
        return f"Book('{self.title}', {self.pages})"

    def __eq__(self, other):
        return self.pages == other.pages

    def __len__(self):
        return self.pages

b1 = Book("Python", 300)
b2 = Book("Java", 300)
print(str(b1))
print(repr(b1))
print(b1 == b2)
print(len(b1))
Output
python
Python Book('Python', 300) True 300
💡 Tip: __repr__ should ideally return a string that could recreate the object — helpful for debugging.
Try it yourself

Add __lt__ to Book so books can be sorted by pages.

Return self.pages < other.pages.
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages
    def __lt__(self, other):
        return self.pages < other.pages
books = [Book("B", 200), Book("A", 100)]
print([b.title for b in sorted(books)])
13

Dataclasses

25 min
What you'll learn
  • Use @dataclass to reduce boilerplate
  • Add default values and fields
  • Convert dataclasses to dicts

Dataclasses automatically generate __init__, __repr__, and __eq__ for simple data-holding classes. You add @dataclass, declare fields with type hints, and get a clean class with defaults and sorting for free.

Python
from dataclasses import dataclass, asdict

@dataclass
class Product:
    name: str
    price: float
    stock: int = 0

p = Product("Laptop", 999.99, 10)
print(p)
print(p == Product("Laptop", 999.99, 10))
print(asdict(p))
Output
python
Product(name='Laptop', price=999.99, stock=10) True {'name': 'Laptop', 'price': 999.99, 'stock': 10}
✓ Best Practice: Use dataclasses for classes that mostly store data — they remove a lot of repetitive boilerplate.
Try it yourself

Create a dataclass Person with name, age, and city defaulting to 'Unknown'.

@dataclass class Person: name: str; age: int; city: str = 'Unknown'.
from dataclasses import dataclass
@dataclass
class Person:
    name: str
    age: int
    city: str = "Unknown"
p = Person("Sufyan", 19)
print(p)
14

Exception Handling Deep Dive

25 min
What you'll learn
  • Create custom exception classes
  • Use raise from to chain errors
  • Use contextlib for clean handling

Beyond try/except, you can define your own exceptions by subclassing Exception. raise from preserves the original error when re-raising. contextlib.suppress cleanly ignores specific errors when you expect them.

Python
class NegativeValueError(ValueError):
    pass

def set_age(age):
    if age < 0:
        raise NegativeValueError("Age cannot be negative")
    return age

try:
    set_age(-5)
except NegativeValueError as e:
    print("Caught:", e)
Output
python
Caught: Age cannot be negative
🔎 Important: Name custom exceptions clearly and inherit from a built-in exception like ValueError — not plain Exception.
Try it yourself

Raise a custom error when a deposit amount is zero.

Define class InvalidDepositError(Exception) and raise it.
class InvalidDepositError(Exception):
    pass

def deposit(amount):
    if amount <= 0:
        raise InvalidDepositError("Amount must be positive")
    return amount

try:
    deposit(0)
except InvalidDepositError as e:
    print(e)
15

File Handling & Pathlib

30 min
What you'll learn
  • Use pathlib for cross-platform paths
  • Read and write CSV and JSON
  • Build context managers with with

pathlib provides an object-oriented, cross-platform way to work with paths — better than raw string paths. Python's csv and json modules handle structured data, and the with statement ensures files close correctly.

Python
from pathlib import Path
import json, csv

base = Path("./data")
base.mkdir(exist_ok=True)

data = {"name": "Sufyan", "age": 19}
(base / "info.json").write_text(json.dumps(data))
print((base / "info.json").read_text())

with (base / "users.csv").open("w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["name", "age"])
    writer.writerow(["Ali", 25])

print((base / "users.csv").exists())
Output
python
{"name": "Sufyan", "age": 19} True
✓ Best Practice: Prefer pathlib over os.path — it's cleaner, safer, and works identically on Windows, macOS, and Linux.
Try it yourself

Create a reports/ folder and write a hello.txt inside it.

Path('reports').mkdir(exist_ok=True) then write_text.
from pathlib import Path
p = Path("reports")
p.mkdir(exist_ok=True)
(p / "hello.txt").write_text("hi")
print((p / "hello.txt").read_text())
16

Regular Expressions

35 min
What you'll learn
  • Understand regex patterns and re functions
  • Use match groups
  • Match emails and phones with patterns

Regular expressions search and match text patterns. re.search finds the first match, re.findall returns all matches, and parentheses create groups. Patterns like \d+ match digits and [a-z]+ match lowercase words.

Python
import re

email = "Contact: [email protected]"
phone = "Call 0300-1234567"

match = re.search(r"([\w.]+)@([\w.]+)", email)
print(match.group(1))
print(match.group(2))

print(re.findall(r"\d+", phone))
Output
python
sufyan example.com ['0300', '1234567']
⚠️ Common Mistake: Regex is powerful but hard to read — use it for clear patterns, and keep the pattern simple with comments when possible.
Try it yourself

Extract all words that start with 'p' from a sentence.

Use re.findall(r'\bp\w+', text).
import re
text = "python is powerful and practical"
print(re.findall(r"\bp\w+", text))
17

Working with APIs

35 min
What you'll learn
  • Make GET requests with requests
  • Parse JSON responses
  • Handle status codes and errors

APIs let your program talk to external services over HTTP. The requests library makes GET calls, the .json() method parses responses, and checking status_code helps you handle failures gracefully.

Python
import requests

try:
    response = requests.get("https://api.github.com/users/programmingpioneer", timeout=10)
    response.raise_for_status()
    data = response.json()
    print("User:", data.get("login"))
    print("Public repos:", data.get("public_repos"))
except requests.RequestException as e:
    print("Request failed:", e)
Output
python
User: programmingpioneer Public repos: 10
🔎 Important: Always use response.raise_for_status() and wrap API calls in try/except — network failures happen.
Try it yourself

Fetch a JSON placeholder post and print its title.

GET https://jsonplaceholder.typicode.com/posts/1 and print data['title'].
import requests
r = requests.get("https://jsonplaceholder.typicode.com/posts/1")
print(r.json()["title"])
18

SQLite with Python

35 min
What you'll learn
  • Connect to a SQLite database
  • Run CRUD operations
  • Use parameterized queries safely

SQLite is a lightweight database built into Python. The sqlite3 module connects to a file, a cursor executes SQL, and parameterized queries (?) prevent SQL injection. CRUD means Create, Read, Update, Delete.

Python
import sqlite3

conn = sqlite3.connect(":memory:")
cursor = conn.cursor()

cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Sufyan",))
cursor.execute("INSERT INTO users (name) VALUES (?)", ("Ali",))
conn.commit()

cursor.execute("SELECT * FROM users")
for row in cursor.fetchall():
    print(row)
conn.close()
Output
python
(1, 'Sufyan') (2, 'Ali')
✓ Best Practice: Never build SQL with string formatting. Always use parameterized queries with ? placeholders.
Try it yourself

Create a books table with title and author, then insert one book.

CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT).
import sqlite3
conn = sqlite3.connect(":memory:")
c = conn.cursor()
c.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, author TEXT)")
c.execute("INSERT INTO books (title, author) VALUES (?, ?)", ("Python Basics", "Sufyan"))
conn.commit()
print(c.execute("SELECT * FROM books").fetchall())
conn.close()
19

Testing with pytest

30 min
What you'll learn
  • Write test functions with assertions
  • Use fixtures for setup
  • Handle expected exceptions

Testing proves your code works. pytest discovers test_* functions and runs assertions. Fixtures provide reusable setup data. pytest.raises checks that errors are raised as expected — the foundation of reliable code.

Python
import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide():
    assert divide(10, 2) == 5
    assert divide(1, 4) == 0.25

def test_divide_by_zero():
    with pytest.raises(ValueError):
        divide(1, 0)

print("All tests passed.")
Output
python
All tests passed.
✓ Best Practice: Write tests alongside every function — it catches bugs early and documents expected behaviour.
Try it yourself

Write a test that checks len('hello') == 5.

def test_length(): assert len('hello') == 5.
def test_length():
    assert len("hello") == 5
20

Capstone: Expense Tracker CLI

45 min
What you'll learn
  • Combine OOP, dataclasses, and SQLite
  • Build a complete CLI application
  • Apply testing to real code

This capstone combines everything: dataclasses for expenses, sqlite3 for storage, pathlib for files, and custom exceptions for validation. You'll build a CLI that adds, lists, and totals expenses — a real-world project.

Python
from dataclasses import dataclass
import sqlite3
from pathlib import Path

@dataclass
class Expense:
    name: str
    amount: float

class ExpenseTracker:
    def __init__(self, db_path="expenses.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute("CREATE TABLE IF NOT EXISTS expenses (id INTEGER PRIMARY KEY, name TEXT, amount REAL)")

    def add(self, name, amount):
        self.conn.execute("INSERT INTO expenses (name, amount) VALUES (?, ?)", (name, amount))
        self.conn.commit()

    def total(self):
        row = self.conn.execute("SELECT COALESCE(SUM(amount), 0) FROM expenses").fetchone()
        return row[0]

    def all(self):
        return self.conn.execute("SELECT name, amount FROM expenses").fetchall()

    def close(self):
        self.conn.close()

app = ExpenseTracker(":memory:")
app.add("Lunch", 500)
app.add("Books", 1200)
print("Total:", app.total())
print("All:", app.all())
app.close()
Output
python
Total: 1700.0 All: [('Lunch', 500.0), ('Books', 1200.0)]
🔎 Important: Separate concerns: dataclasses for data, a class for storage, and small methods for each feature.
Try it yourself

Add a delete(name) method that removes an expense by name.

Execute DELETE FROM expenses WHERE name = ? and commit.
def delete(self, name):
    self.conn.execute("DELETE FROM expenses WHERE name = ?", (name,))
    self.conn.commit()

Intermediate Projects

Build real tools with the skills you've learned. Click each project to open its full guide.

About this project

Build a simple CLI task scheduler that stores tasks with due dates and priorities, then sorts them so the most urgent work appears first.

What you'll practice
*args/**kwargs datetime sorted() functions lists
Step-by-step: How it works
  1. add_task() collects a task name, due date, and priority level.
  2. Each task is stored as a dictionary with all three pieces.
  3. sort_tasks() uses sorted() with a key that sorts by date first, then priority.
  4. The display loop prints tasks in the sorted order with a clean format.
  5. Running the example shows how the scheduler orders mixed priorities.
Python
from datetime import date

tasks = []

def add_task(title, due, priority):
    tasks.append({"title": title, "due": due, "priority": priority})

def sort_tasks():
    return sorted(tasks, key=lambda t: (t["due"], t["priority"]))

add_task("Write report", date(2026, 8, 25), 2)
add_task("Fix login bug", date(2026, 8, 23), 1)
add_task("Plan sprint", date(2026, 8, 23), 3)

for task in sort_tasks():
    print(f"{task['due']} — P{task['priority']} — {task['title']}")
Sample Run
python task_scheduler.py
2026-08-23 — P1 — Fix login bug 2026-08-23 — P3 — Plan sprint 2026-08-25 — P2 — Write report

About this project

Fetch data from a public JSON API and display the first few posts cleanly. It teaches safe network requests and JSON parsing.

What you'll practice
requests JSON parsing error handling dictionaries status codes
Step-by-step: How it works
  1. requests.get() fetches the API response.
  2. We check status_code == 200 before using the data.
  3. .json() parses the response into a list of dictionaries.
  4. We slice the first 3 results and print the title and body for each.
  5. A try/except block catches network errors gracefully.
Python
import requests

url = "https://jsonplaceholder.typicode.com/posts"

try:
    response = requests.get(url)

    if response.status_code != 200:
        print("Request failed:", response.status_code)
    else:
        posts = response.json()

        for post in posts[:3]:
            print("Title:", post["title"])
            print("Body:", post["body"][:60], "...")
            print("-" * 40)

except requests.RequestException as err:
    print("Network error:", err)
Sample Run
python api_explorer.py
Title: sunt aut facere repellat... Body: quia et suscipit suscipit recusandae... ---------------------------------------- Title: qui est esse Body: est rerum tempore vitae sequi sint... ---------------------------------------- Title: ea molestias quasi exercitationem... Body: et iusto sed quo iure voluptatem...

About this project

Store contacts permanently in an SQLite database. Add, view, and delete contacts using a clean class that wraps the database.

What you'll practice
sqlite3 classes CRUD parameterized queries pathlib
Step-by-step: How it works
  1. ContactBook.__init__ creates a database and the contacts table if needed.
  2. add() inserts a new row using a parameterized query.
  3. view_all() fetches every contact with fetchall().
  4. delete() removes a contact by name safely.
  5. The example adds two contacts, prints them, then deletes one.
Python
import sqlite3

class ContactBook:
    def __init__(self, db="contacts.db"):
        self.conn = sqlite3.connect(db)
        self.conn.execute(
            "CREATE TABLE IF NOT EXISTS contacts (name TEXT, phone TEXT)"
        )

    def add(self, name, phone):
        self.conn.execute(
            "INSERT INTO contacts VALUES (?, ?)", (name, phone)
        )
        self.conn.commit()

    def view_all(self):
        return self.conn.execute(
            "SELECT name, phone FROM contacts ORDER BY name"
        ).fetchall()

    def delete(self, name):
        self.conn.execute(
            "DELETE FROM contacts WHERE name = ?", (name,)
        )
        self.conn.commit()

    def close(self):
        self.conn.close()

book = ContactBook(":memory:")
book.add("Ali", "0300-1234567")
book.add("Sufyan", "0301-7654321")

print(book.view_all())

book.delete("Ali")
print(book.view_all())

book.close()
Sample Run
python contact_book.py
[('Ali', '0300-1234567'), ('Sufyan', '0301-7654321')] [('Sufyan', '0301-7654321')]
You've completed all 20 lessons. Ready to continue?

Continue to Python Advanced for professional-grade concepts.

📱 Scan this QR code with your phone camera to instantly open this page.

Works on iOS, Android, and any modern device. No app installation required.

Account Verified!

Your email has been verified successfully.