A terminal-based Python simulation of a school. A local LLM (Ollama) generates the content β courses, material, exams, students, newsletters. You provide the seed: a city, a subject, and a few parameters. The simulation runs itself.
Built as a learning project for OOP in Python, and a proof of concept that OOP can model surprisingly complex real-world logic.
This project is complete. It expresses what I set out to explore β OOP, local LLMs, and newsletter design β and I'm moving on to other things. The remaining headroom is prompt optimisation, which isn't where my interest lies right now.
I've delivered training courses professionally β the process was often the same: a domain expert provides the subject, a writer turns it into material, a testing expert writes an exam based on that material. This project explores how much of that process a local AI can assist with:
- Subject β you pitch it, or the AI invents one
- Material β you write it, or the AI drafts it (and you can review it on screen)
- Exam β the AI writes questions from the material, so they're grounded in what was taught
- Simulation β the AI populates the school and runs it
A second thread runs through this project: I've spent time making newsletters, and there's always an algorithm behind them. A principle I keep coming back to is borrowed from observability engineering β if you send engineers too many alerts, they start ignoring all of them. A newsletter that costs more to produce than it delivers in value isn't a newsletter, it's noise.
The approach here is to separate the two jobs cleanly: let the data tell you what happened (counts, sums, comparisons), then let the AI narrate it. The data comes first. The AI fills in the human voice around it.
And hereforth we have a newsletter. You can tell your team to start their weekend early - the King is in town and the newsletter is scheduled to be sent, based on a bunch of stuff they did on Monday and Tuesday.
# Install Ollama (macOS)
brew install ollama
# Pull the recommended model (4.4GB)
ollama pull mistral
# Start the Ollama server
ollama servegit clone https://github.com/jackwaddington/python_school.git
cd python_school
# Create and activate a virtual environment
python3 -m venv venv
source venv/bin/activate
pip install requests matplotlib pandas
python3 src/main.pyOther models (llama3.2, phi3, gemma2:2b) should work but are little tested. Ollama defaults to localhost:11434.
- You answer a few setup questions (city, subject, Ollama connection)
- You choose auto or semi-auto mode
- Structural parameters use sensible defaults β override in semi-auto if you want
- The AI generates courses, material, exams, and students
- The simulation runs for however many weeks you set
- A timestamped output folder is created with newsletters, student profiles, course materials, and CSVs
In full auto, the minimum input is: city, subject, and press Enter for everything else. And start your weekend early.
The Helsinki School of Tyre Changing/ folder is a complete example run:
- 200 weeks (~4 years)
- 3 courses: Tyre Change Basics, Tyre Mounting and Dismounting Skills, Advanced Tyre Swap Techniques
- 4 cohorts, 120 students enrolled across the simulation
- 200 weekly newsletters + 46 monthly newsletters
students.csv,weekly_stats.csv,simulation_report.png
| Parameter | Default | Notes |
|---|---|---|
| City | Helsinki | Used in the school name |
| Subject | AI-generated | The field of study |
| Ollama host | localhost:11434 | |
| Ollama model | mistral |
| Parameter | Default | What it controls |
|---|---|---|
| Seats | 20 | School capacity |
| Number of courses | 3 | Courses created |
| Courses to graduate | = num courses | Graduation threshold |
| Min weeks per course | 4 | Lower bound of course duration |
| Max weeks per course | 12 | Upper bound of course duration |
| Intake interval | 52 weeks | How often a new cohort arrives |
| Total weeks | 200 | Length of simulation (~4 years) |
| Variable | Value | What it controls |
|---|---|---|
student.ability |
random 0.3β0.9 | Probability of passing each weekly exam |
| Rough cohort ability | random 0.1β0.4 | Ability range when a cohort is flagged as rough |
| Rough cohort chance | 15% per new intake | Probability each new cohort is a disaster |
| Base dropout rate | 0.4% per week | ~55% cumulative dropout over 200 weeks |
| Strike dropout spike | 8% per week | Dropout rate during a teacher strike |
| Funding cut rate | 1% per week | Chance of losing 20% of seats that week |
| Teacher strike rate | 0.5% per week | Chance of a 3-week exam blackout starting |
| Strike duration | 3 weeks | How long a strike lasts |
| Students per seat | Γ1.1 | First cohort size |
| Batch size | 10 students | Students generated per LLM call |
| Simulation start date | 2026-01-05 | Calendar date for week 1 |
Every week the simulation writes a newsletter to disk. Every 4 weeks, a monthly edition too.
The prompt is structured in two parts:
- Hard facts β numbers, disasters, new graduates, ceremony countdowns β hardcoded into the prompt
- AI narrative β 3 short sections written by the principal (This Week, Message from the Principal, Next Week)
The AI is given the principal's voice and told to react to the data, not just restate it.
Each run creates a timestamped folder:
YYYYMMDD_HHMM_CitySchoolOfSubject/
school_profile.txt
weekly_stats.csv
students.csv
courses/
Course_Name/
material.txt
exam.txt
students/
cohort_01/
First_Last/
profile.txt
weekly_newsletters/
week_001.txt
week_002.txt
...
monthly_newsletters/
2026_01_Jan_Newsletter.txt
...
Output folders are gitignored automatically.
| File | Class | Key attributes |
|---|---|---|
student.py |
Student |
ability, course_index, course_passes, graduated, dropped_out, cohort |
course.py |
Course |
subject, field, material, exam, enrolled_students, required_passes |
school.py |
School |
name, description, seats, students, courses, cohorts, events |
cohort.py |
Cohort |
number, intake_week, students, is_rough |
event.py |
Event |
date, title, description |
llm_client.py |
LLMClient |
host, model, generate(prompt) |
helpers.py |
β | generate_students(), ask_or_generate(), ask_or_default(), parse_json_response() |
setup.py |
β | run_setup() β school wizard, returns school + config |
simulation.py |
β | run_simulation() β weekly loop, events, newsletters |
newsletters.py |
β | generate_weekly_newsletter(), generate_monthly_newsletter() |
records.py |
β | create_course_files(), create_student_file() |
export.py |
β | save_csv(), print_summary() |
exam_runner.py |
β | run_exam_for_user() β interactive MCQ for the user |