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
| Test input or action | Expected result |
|---|---|
| Run with .venv interpreter | Executable path identifies .venv; virtual environment True |
| Run with base interpreter | False for a normal base Python; different executable |
| New terminal | Selection 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.
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
| Test input or action | Expected result |
|---|---|
| Parent folder plus bare location_check.py | File not found if that parent has no such file |
| Inside terminal practice | Runs and shows the current folder and script path |
| Absolute script path from another folder | Script 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.
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
| Test input or action | Expected result |
|---|---|
| Correct lab environment | Import succeeds and path points to its installed package |
| Environment without Requests | ModuleNotFoundError; install into the chosen interpreter |
| Rebuilt environment | Import 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.
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
| Test input or action | Expected result |
|---|---|
| No local shadow, Requests installed | Package path; get exists |
| Local requests.py with only marker | Local path; get is absent |
| Rename local module and restart | Installed 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.
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
| Test input or action | Expected result |
|---|---|
| Before second commit | Index version two; working file version three |
| Committed file | Version two |
| Working file after commit | Version 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.
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
| Test input or action | Expected result |
|---|---|
| Clean branch operation | Feature commit merges; report still runs |
| Untracked .venv matching ignore | Not offered as an ordinary untracked source file |
| Already tracked file later ignored | Still 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.
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
| Test input or action | Expected result |
|---|---|
| Noor apostrophe title | Exact title is stored and found |
| SQL-looking second title | Stored literally; table remains |
| Record count | 2 |
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.
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
| Test input or action | Expected result |
|---|---|
| Duplicate operation | IntegrityError handled; zero rows remain |
| Later valid insert | One committed row |
| Connection exit | Closed 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.
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
| Test input or action | Expected result |
|---|---|
| /api/books | Valid first page, two items |
| /api/error | HTTP failure: 503 |
| /api/broken | JSON decoding failure |
| /api/wrong-shape | Schema 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.
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
| Test input or action | Expected result |
|---|---|
| Normal collection | IDs [1, 2, 3] |
| Maximum one page | ValueError before requesting another page |
| No next page | Collection 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.
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
| Test input or action | Expected result |
|---|---|
| New store, same file | Saved task is present |
| Due date / following date | Not overdue / overdue while open |
| Completed task | Excluded from overdue |
| Reopened task | Present 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.
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
| Test input or action | Expected result |
|---|---|
| Duplicate project | Rejected; exactly one project remains |
| No tasks | Project counts show zero, not one |
| Clean handover | Source and tests run; no copied .venv needed |
| Quit and restart real app | Earlier 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.
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