LEARN PYTHON | 07 DEVELOPER TOOLS AND DATABASES

Build it. Test it. Explain it.

Create a dependable project workflow, then build an application that remembers its data. Work independently before opening a hint, then compare your tested solution with one possible approach.

YOUR CHALLENGE ROUTE

Complete these tasks in your Python editor. The page is a challenge guide, not a general Python runner. Use fictional details where helpful. Work in order or choose a task matching an area you need to practise.

These tasks combine developer tools with earlier Python skills. The Git samples are terminal commands; other samples are Python source. API tasks require the local API lab and Requests. Planner tasks require store.py from its companion download. Work in disposable practice folders and fictional records.

TRACK YOUR TESTED SOLUTIONS

Tick a task only after you have run it, checked at least two relevant cases and explained one line. These are self-recorded checks, not an automatic assessment of your source code.

Progress lasts while this page is open. Save your actual Python files in your project folder.

01 | ENVIRONMENT FOUNDATIONS

PROVE YOUR INTERPRETER

Create a project-specific virtual environment and verify it through Python rather than only trusting the prompt.

YOUR REQUIREMENTS

  • Create .venv with the intended Python 3.10+ interpreter.
  • Activate it or use its explicit executable path.
  • Save interpreter_check.py with the sample below.
  • Run the file and record sys.executable.
  • Open a second terminal and compare interpreter selection.
Hint: where to begin

The terminal creates and activates the environment. The source below belongs in a .py file, not in the shell as a single command.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Run with .venv interpreterExecutable path identifies .venv; virtual environment True
Run with base interpreterFalse for a normal base Python; different executable
New terminalSelection depends on that session, not the earlier prompt
Reveal one possible solution and explanation

PYTHON SOURCE

import sys
from pathlib import Path

print("Executable:", sys.executable)
print("Version:", sys.version.split()[0])
print("Virtual environment:", sys.prefix != sys.base_prefix)
print("Working directory:", Path.cwd())

The executable and prefixes give direct evidence of selection. A working directory is separate from the interpreter path: selecting .venv does not automatically move into the source folder.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Run without activation through an explicit .venv path and explain why it still uses the correct package area.

Revisit the related lesson

02 | TERMINAL DIAGNOSIS

REPAIR A WRONG-FOLDER RUN

Create a file in a folder containing a space, then diagnose a failed relative-path run from its parent folder.

YOUR REQUIREMENTS

  • Create a disposable folder named terminal practice.
  • Save location_check.py inside it.
  • Try a relative filename from the parent and record the file-not-found result.
  • Enter the quoted folder and rerun the file.
  • Explain which location Python resolved the filename against.
Hint: where to begin

Use Get-Location in PowerShell or pwd in bash/zsh to inspect the current folder. The sample is Python source; cd is a separate terminal command.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Parent folder plus bare location_check.pyFile not found if that parent has no such file
Inside terminal practiceRuns and shows the current folder and script path
Absolute script path from another folderScript path stays fixed; working directory may differ
Reveal one possible solution and explanation

PYTHON SOURCE

from pathlib import Path

print("Current folder:", Path.cwd())
print("This file:", Path(__file__).resolve())

The script location and process working directory need not match. This is why the planner places its database relative to __file__ instead of relying on where a terminal was opened.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Compare Path("notes.txt") with Path(__file__).resolve().parent / "notes.txt" without overwriting an existing file.

Revisit the related lesson

03 | PACKAGE RECIPE

MAKE A REBUILDABLE API ENVIRONMENT

Set up the downloaded API lab and leave another student a tested requirements recipe.

YOUR REQUIREMENTS

  • Create a clean .venv.
  • Install the lab requirements.txt using python -m pip.
  • Record interpreter and requests versions.
  • Run python -m pip check.
  • Freeze a tested snapshot and try it in a separate clean environment.
  • Keep environments out of the handover.
Hint: where to begin

Run the shell installation commands from 07.2 before this Python inspection file. Freeze captures installed dependencies; also document runtime and platform assumptions.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Correct lab environmentImport succeeds and path points to its installed package
Environment without RequestsModuleNotFoundError; install into the chosen interpreter
Rebuilt environmentImport and local-lab test suite succeed
Reveal one possible solution and explanation

PYTHON SOURCE

import sys
import requests

print("Python:", sys.version.split()[0])
print("Requests:", requests.__version__)
print("Loaded package:", requests.__file__)

The version report and package path complement pip’s own dependency check. A frozen version list is a recipe component, not a guarantee of identical behaviour on every operating system.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Compare the one-line direct range file with the frozen transitive snapshot and explain why their contents differ.

Revisit the related lesson

04 | PACKAGE REPAIR

DIAGNOSE SHADOWING WITHOUT RANDOM INSTALLS

In a disposable folder, investigate why a file named requests.py can mask the installed distribution.

YOUR REQUIREMENTS

  • First run the inspection source with no conflicting file.
  • Create a separate requests.py containing marker = "local".
  • Rerun the inspection in a new interpreter process.
  • Rename that local file and restart the interpreter.
  • Explain why reinstalling an unrelated package is not the repair.
Hint: where to begin

Inspect __file__ to find the loaded module. Restarting avoids confusing cached imports from the earlier process.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
No local shadow, Requests installedPackage path; get exists
Local requests.py with only markerLocal path; get is absent
Rename local module and restartInstalled package is imported again
Reveal one possible solution and explanation

PYTHON SOURCE

import requests

print("Loaded module:", requests.__file__)
print("Has HTTP get:", hasattr(requests, "get"))

Python’s import search can find a project module first. The sample deliberately uses hasattr so inspection itself does not fail when the stub lacks get.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Write a troubleshooting checklist distinguishing missing installation, wrong interpreter and local module shadowing.

Revisit the related lesson

05 | GIT WORKFLOW

RECORD THE STAGED SNAPSHOT

Use a disposable local repository to prove that an unstaged edit is not included merely because the file was staged earlier.

YOUR REQUIREMENTS

  • Initialise and configure local practice Git.
  • Commit app.py at greeting version one.
  • Edit to version two and git add app.py.
  • Edit again to version three without adding.
  • Inspect git diff and git diff --cached.
  • Commit and compare git show HEAD:app.py with the working file.
Hint: where to begin

The sample below is terminal commands, not Python. Run it in a clean disposable repository after creating and staging the stated edits.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Before second commitIndex version two; working file version three
Committed fileVersion two
Working file after commitVersion three remains an unstaged change
Reveal one possible solution and explanation

TERMINAL COMMANDS

git status
git diff
git diff --cached
git commit -m "Record greeting version two"
git show HEAD:app.py
git status

A commit records the index. This task deliberately shows a commit and working file with different content; do not describe the entire current working folder as recorded.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Stage and commit version three, then explain both changes through git log --oneline.

Revisit the related lesson

06 | GIT AND HANDOVER

BRANCH, TEST AND MERGE A REPORT

Create a feature branch for a summary message and merge its tested commit into the original branch.

YOUR REQUIREMENTS

  • Start with a clean practice repository.
  • Record its current branch name.
  • Create report-feature.
  • Edit and test app.py before committing.
  • Return to the actual original branch and merge.
  • Create .gitignore and verify a generated .venv file stays untracked.
Hint: where to begin

Replace BASE_BRANCH with the name you recorded; it is not a literal universal branch. The sample is shell commands, with the edit and test performed between switch and add.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Clean branch operationFeature commit merges; report still runs
Untracked .venv matching ignoreNot offered as an ordinary untracked source file
Already tracked file later ignoredStill tracked: ignore is not a history-removal tool
Reveal one possible solution and explanation

TERMINAL COMMANDS

git branch --show-current
git switch -c report-feature
git add app.py
git commit -m "Add tested summary message"
git switch BASE_BRANCH
git merge report-feature
git log --oneline

The branch preserves a line of recorded work; it does not create an independently backed-up folder. Test the merged result as well as the feature branch.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Create competing edits in a second disposable exercise, resolve the conflict, test and complete the merge.

Revisit the related lesson

07 | SQL PARAMETERS

STORE APOSTROPHES AS DATA

Create a disposable book table and store titles containing apostrophes and SQL-looking characters without assembling SQL from their text.

YOUR REQUIREMENTS

  • Use placeholders for both inserted titles.
  • Query one exact title with a one-item tuple.
  • Verify two original rows still exist.
  • Order results explicitly.
  • Close the connection.
Hint: where to begin

A parameter is a value, not a fragment of a WHERE clause. The comma in (title,) matters.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Noor apostrophe titleExact title is stored and found
SQL-looking second titleStored literally; table remains
Record count2
Reveal one possible solution and explanation

PYTHON SOURCE

from contextlib import closing
import sqlite3

with closing(sqlite3.connect(":memory:", isolation_level="DEFERRED")) as con:
    with con:
        con.execute("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT NOT NULL)")
        con.executemany("INSERT INTO books (title) VALUES (?)", [("Noor's Notes",), ("x'); DROP TABLE books; --",)])
    title = "Noor's Notes"
    print(con.execute("SELECT id, title FROM books WHERE title = ? ORDER BY id", (title,)).fetchall())
    print("Stored rows:", con.execute("SELECT COUNT(*) FROM books").fetchone()[0])

Binding keeps data separate from instructions. This test is not evidence of encryption, access control or truth verification: it checks the precise query-construction behaviour.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Query the SQL-looking title itself through a bound value and check that it returns exactly one row.

Revisit the related lesson

08 | TRANSACTIONS

ROLL BACK A TWO-STEP FAILURE

Attempt two inserts in one transaction where the second breaks a uniqueness constraint. Prove that neither insert from that transaction remains.

YOUR REQUIREMENTS

  • Create a table with a unique name.
  • Begin the operation and insert the same name twice.
  • Catch IntegrityError outside the transaction context.
  • Count remaining rows.
  • Perform a valid insert afterwards.
Hint: where to begin

If the error escapes with con, the pending inserts roll back together. Schema creation is a separate setup step here.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Duplicate operationIntegrityError handled; zero rows remain
Later valid insertOne committed row
Connection exitClosed by closing context
Reveal one possible solution and explanation

PYTHON SOURCE

from contextlib import closing
import sqlite3

with closing(sqlite3.connect(":memory:", isolation_level="DEFERRED")) as con:
    con.execute("CREATE TABLE projects (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE)")
    try:
        with con:
            con.execute("INSERT INTO projects (name) VALUES (?)", ("Robotics",))
            con.execute("INSERT INTO projects (name) VALUES (?)", ("Robotics",))
    except sqlite3.IntegrityError:
        print("Rejected and rolled back")
    print("After rejection:", con.execute("SELECT COUNT(*) FROM projects").fetchone()[0])
    with con:
        con.execute("INSERT INTO projects (name) VALUES (?)", ("Coding",))
    print("After valid save:", con.execute("SELECT COUNT(*) FROM projects").fetchone()[0])

The transaction protects related changes as a unit. Catching the exception after the context has exited makes the rollback decision clear.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Extend the exercise to related projects and tasks with foreign keys enabled before starting transactions.

Revisit the related lesson

09 | HTTP LAB

CLASSIFY API FAILURES

Run this script beside client.py with the local server running. Classify success, HTTP failure, malformed JSON and wrong-shaped JSON.

YOUR REQUIREMENTS

  • Install Requests in the active environment.
  • Start server.py in another terminal.
  • Check four controlled endpoints.
  • Catch JSONDecodeError before RequestException.
  • Do not replace a failure with an empty successful collection.
Hint: where to begin

fetch_page already checks HTTP status, JSON decoding and schema. The handler order distinguishes the failure layers.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
/api/booksValid first page, two items
/api/errorHTTP failure: 503
/api/brokenJSON decoding failure
/api/wrong-shapeSchema failure
Reveal one possible solution and explanation

PYTHON SOURCE

import requests
from client import fetch_page

for path in ["/api/books", "/api/error", "/api/broken", "/api/wrong-shape"]:
    try:
        data = fetch_page("http://127.0.0.1:8765" + path)
    except requests.exceptions.JSONDecodeError:
        print(path, "invalid JSON")
    except requests.exceptions.RequestException:
        print(path, "HTTP or network failure")
    except ValueError:
        print(path, "schema failure")
    else:
        print(path, "valid page with", len(data["items"]), "items")

A usable reply passes all layers. JSONDecodeError is caught first because Requests also classifies it within the more general request exception hierarchy.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Stop the local server and repeat one request. Explain why this network failure is not a valid empty catalogue.

Revisit the related lesson

10 | API PAGINATION

FETCH EVERY PAGE WITH A LIMIT

Collect both lab catalogue pages, then prove a deliberately small page limit rejects the incomplete operation.

YOUR REQUIREMENTS

  • Use the provided fetch_all helper.
  • Expect three unique IDs in the normal report.
  • Use max_pages=1 for a second request.
  • Handle the expected ValueError.
  • Explain repeated-page and duplicate-ID checks.
Hint: where to begin

Run beside client.py with server.py running. The limit bounds page count; Requests timeouts are separate waits.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Normal collectionIDs [1, 2, 3]
Maximum one pageValueError before requesting another page
No next pageCollection finishes normally rather than requesting forever
Reveal one possible solution and explanation

PYTHON SOURCE

from client import fetch_all

url = "http://127.0.0.1:8765/api/books"
books = fetch_all(url)
print("IDs:", [book["id"] for book in books])
try:
    fetch_all(url, max_pages=1)
except ValueError:
    print("Incomplete collection rejected by page limit")

The helper returns a full checked collection or raises; it does not present a partial list as complete. This lab uses page numbers; another service may require a different contract.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add isolated tests supplying repeated-page and duplicate-ID responses, without depending on a public server.

Revisit the related lesson

11 | PERSISTENCE CAPSTONE

BUILD A PLANNER THAT SURVIVES RESTART

Run this check file beside store.py to verify persistence through a new store object, a precise overdue boundary and completion filtering.

YOUR REQUIREMENTS

  • Use a temporary folder so real planner data is untouched.
  • Create one project and task.
  • Construct another TaskStore for the same file.
  • Check due day versus following day.
  • Complete and reopen the task.
  • Run app.py separately, quit and restart it.
Hint: where to begin

A new TaskStore reading the same file tests persistence independently of the console. The second manual process test confirms the user-facing workflow.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
New store, same fileSaved task is present
Due date / following dateNot overdue / overdue while open
Completed taskExcluded from overdue
Reopened taskPresent in open report
Reveal one possible solution and explanation

PYTHON SOURCE

from pathlib import Path
from tempfile import TemporaryDirectory
from store import TaskStore

with TemporaryDirectory() as folder:
    path = Path(folder) / "test.sqlite3"
    first = TaskStore(path)
    project_id = first.add_project("Robotics")
    task_id = first.add_task(project_id, "Test motors", "2026-10-15")
    second = TaskStore(path)
    assert len(second.tasks()) == 1
    assert second.overdue("2026-10-15") == []
    assert len(second.overdue("2026-10-16")) == 1
    second.set_done(task_id, True)
    assert second.overdue("2026-10-16") == []
    second.set_done(task_id, False)
    assert len(second.tasks("open")) == 1
print("Persistence and boundary checks passed")

The store’s committed file survives the object’s lifetime. TemporaryDirectory cleans the test file only after this exercise finishes; the real app’s planner.sqlite3 is retained.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Add a second task on the same due date and assert the report order uses task ID as the tie-breaker.

Revisit the related lesson

12 | PROJECT QUALITY

TEST REJECTION AND HAND OVER

Prove a duplicate project does not damage saved data, then prepare a clean source handover with tests and data-lifetime instructions.

YOUR REQUIREMENTS

  • Use temporary data for the automated check.
  • Reject a duplicate project name.
  • Verify the original still exists.
  • Include an empty project in counts.
  • Run all 14 store tests.
  • Document environment, entry point, local data path, closed-app backup and schema limitations.
Hint: where to begin

Duplicate names are rejected by SQLite uniqueness. Catch the expected IntegrityError and then inspect accepted state.

TEST YOUR PROGRAM

Check predictions against actual results
Test input or actionExpected result
Duplicate projectRejected; exactly one project remains
No tasksProject counts show zero, not one
Clean handoverSource and tests run; no copied .venv needed
Quit and restart real appEarlier committed records still load
Reveal one possible solution and explanation

PYTHON SOURCE

from pathlib import Path
from tempfile import TemporaryDirectory
import sqlite3
from store import TaskStore

with TemporaryDirectory() as folder:
    store = TaskStore(Path(folder) / "test.sqlite3")
    store.add_project("Robotics")
    try:
        store.add_project("Robotics")
    except sqlite3.IntegrityError:
        print("Duplicate rejected")
    else:
        raise AssertionError("Duplicate should fail")
    assert len(store.projects()) == 1
    report = store.counts()
    assert report[0]["task_count"] == 0
    assert report[0]["completed"] == 0
print("Original project and empty counts preserved")

LEFT JOIN retains the project while COUNT(task ID) avoids counting its empty joined row as a task. Passing rejection tests matters as much as one successful save.

A different implementation can also be correct if it meets the requirements and tests. Compare meaning and behaviour, rather than only copying the source.

Extension: Specify priorities or deletion as a written requirement, then design tests and a migration before changing an existing schema.

Revisit the related lesson

KEEP A SMALL TEST RECORD

Record environment and working directory, input or action, expected result, actual result and whether saved state changed. For a failed transaction, inspect the data afterwards instead of checking only an error message.

Distinguish working files from staged and committed versions; HTTP failure from invalid JSON and invalid schema; and page-session illustrations from persistent database files. Keep the source handover reproducible with tested instructions.

READY FOR THE NEXT SECTION?

You are ready to move on when you can choose the intended interpreter, recreate a package setup, explain staged commits, bind SQL values, classify API failures and demonstrate database persistence.

If one skill is uncertain, revisit its lesson and change a worked example before repeating the challenge. The Section 7 workbook is coming soon.

Next: Tkinter windows, widgets and layouts Python course navigation