Football Tournament Engine (football-tournament)
A production-ready, reusable Python library for managing football tournaments.
football-tournament is designed as a standalone domain engine. It encapsulates all core tournament management logic (team registration, stadium management, match generation, scheduling, conflict detection, result recording, league standings, and knockout bracket progression) without coupling to any web framework, ORM, database, or UI layer.
It can be seamlessly integrated into REST APIs (FastAPI, Django), university systems, mobile applications, or CLI tools.
Features
- Multiple Tournament Formats:
- League / Table: Round-robin (single and double round-robin).
- Knockout: Single-elimination brackets with automatic bye handling for non-power-of-two team counts.
- Group + Knockout: Group stage round-robins followed by automatic qualification and knockout bracket generation.
- Robust Scheduling Engine:
- Manual and automatic scheduling based on configurable time windows (
SchedulingConfig). - Strict conflict detection: prevents stadium double-booking, team overlapping matches, self-play, and invalid time slots.
- Manual and automatic scheduling based on configurable time windows (
- Standings Calculator:
- Computes Played, Won, Drawn, Lost, Goals For, Goals Against, Goal Difference, and Points.
- Configurable points system and tie-breaking rules (
points,goal_difference,goals_for,goals_against,wins,played,head_to_head,name).
- Results & Knockout Progression:
- Records scores and optional penalty shoot-outs; penalty results decide the winner of a drawn knockout tie.
- Winners are auto-advanced into the next round and the bracket is exposed as ordered rounds via
get_bracket(). - Matches can be cancelled; cancelled matches cannot be re-scored.
- Tournament Lifecycle & Validation:
- State management (
DRAFT,REGISTRATION,SCHEDULED,ONGOING,COMPLETED,CANCELLED) with an enforced state machine viaset_status(); tournaments auto-complete when every match is done. - Comprehensive Pydantic validation, config validation, and custom domain exceptions.
- State management (
- Persistence:
save_tournament()/load_tournament()(or theengine.save()/engine.load()convenience wrappers) serialize a full tournament to a JSON file that survives process restarts.
- Framework & Database Independent:
- Works entirely in memory with pure Python objects.
- Easily adaptable to SQLAlchemy, SQLModel, PostgreSQL, SQLite, or Supabase via persistence adapters.
Installation
pip install football-tournament
For development (testing and linting):
pip install -e ".[dev]"
Quick Start
from datetime import date, time
from football_tournament import (
TournamentEngine,
TournamentFormat,
Team,
Stadium,
SchedulingConfig,
)
# 1. Initialize engine
engine = TournamentEngine()
# 2. Create tournament
tournament = engine.create_tournament(
name="University Football Tournament",
format=TournamentFormat.LEAGUE,
)
# 3. Register teams
teams = [
Team(name="Software Engineering"),
Team(name="Computer Science"),
Team(name="Electrical Engineering"),
Team(name="Civil Engineering"),
]
for team in teams:
engine.add_team(tournament, team)
# 4. Add stadium
stadium = Stadium(name="Main Stadium", location="Adama", capacity=5000)
engine.add_stadium(tournament, stadium)
# 5. Generate matches
matches = engine.generate_matches(tournament)
# 6. Automatically schedule matches
config = SchedulingConfig(
start_date=date(2026, 10, 1),
end_date=date(2026, 10, 5),
daily_start=time(14, 0),
daily_end=time(18, 0),
)
engine.auto_schedule(tournament, config)
# 7. Record match result
engine.set_match_result(tournament, matches[0].id, home_score=2, away_score=1)
# 8. View standings
standings = engine.get_standings(tournament)
for rec in standings:
print(f"{rec.team.name}: {rec.points} pts (GD: {rec.goal_difference})")
Knockout with penalties, bracket and persistence
from football_tournament import TournamentEngine, TournamentFormat, Team
engine = TournamentEngine()
tournament = engine.create_tournament(name="Champions Cup", format=TournamentFormat.KNOCKOUT)
for name in ["Real Madrid", "Bayern", "Liverpool", "PSG"]:
engine.add_team(tournament, Team(name=name))
engine.generate_matches(tournament)
# First semifinal ends level; penalties decide the winner.
sf = tournament.matches[0]
engine.set_match_result(
tournament, sf.id, home_score=1, away_score=1,
home_penalties=4, away_penalties=3, # home team advances
)
# The winner of that semifinal is now slotted into the Final.
import json
bracket = engine.get_bracket(tournament) # list[Round]
print([round.name for round in bracket]) # e.g. ["Semifinal", "Final"]
# Persist the whole tournament to disk and reload it later.
path = engine.save(tournament, "champions_cup.json")
reloaded = engine.load(path)
assert reloaded.name == "Champions Cup"
See engine.get_standings(tournament, tiebreakers=["points", "head_to_head"]) for
custom tie-breaking, and engine.cancel_match(...) / tournament.set_status(...) for
cancellation and lifecycle control.
Architecture Overview
football_tournament/
├── models/ # Domain entities (Tournament, Team, Stadium, Match, Group, Round)
├── enums/ # Controlled vocabularies (TournamentFormat, MatchStatus, TournamentStatus)
├── formats/ # Format strategies (League, Knockout, Group + Knockout)
├── scheduling/ # Scheduler, conflict detectors, time slots, scheduling config
├── standings/ # Standings calculator and ranking rules
├── exceptions/ # Custom domain errors
├── persistence.py # JSON save/load helpers
└── engine.py # High-level TournamentEngine facade
Separation of Responsibilities
- Match Generation: Creates fixture pairings without date/time/stadium bindings.
- Scheduling: Assigns time slots and stadiums while enforcing strict conflict rules.
- Results: Records scores and updates match status.
- Standings: Computes table statistics from completed matches.
Integrating with FastAPI
Because the package is framework-independent, integrating it into FastAPI is straightforward:
from fastapi import FastAPI, HTTPException
from football_tournament import TournamentEngine, TournamentFormat, Team, Stadium
app = FastAPI()
engine = TournamentEngine()
# In-memory store for demonstration (replace with database adapter in production)
tournaments = {}
@app.post("/tournaments")
def create_tournament(name: str, format: TournamentFormat):
tournament = engine.create_tournament(name=name, format=format)
tournaments[tournament.id] = tournament
return {"id": tournament.id, "name": tournament.name, "format": tournament.format}
@app.post("/tournaments/{tournament_id}/teams")
def add_team(tournament_id: str, name: str):
if tournament_id not in tournaments:
raise HTTPException(status_code=404, detail="Tournament not found")
t = tournaments[tournament_id]
team = Team(name=name)
engine.add_team(t, team)
return {"team_id": team.id, "name": team.name}
Running Tests
Run the test suite with pytest:
PYTHONPATH=src pytest
Building & Publishing
Build Wheel and Source Distribution
pip install build
python -m build
Publish to PyPI
pip install twine
python -m twine upload dist/*
License
MIT License. See LICENSE for details.
Release files for football-tournament 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| football_tournament-0.2.0.tar.gz | 34.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| football_tournament-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.9 kB
Release files / football_tournament-0.2.0.tar.gz
| Download URL | football_tournament-0.2.0.tar.gz |
|---|---|
| Size | 34.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d0f024c6b11b4acf9ae55cb799cf5743cfc33b50d253968e681ccb21d6bfe84
|
|
BLAKE2b-256 checksum How to use checksums |
b9c6aa45cc7a9ebba17310a89bdcbe8766abe925f9e3c29983b2347d7c4212b4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / football_tournament-0.2.0-py3-none-any.whl
| Download URL | football_tournament-0.2.0-py3-none-any.whl |
|---|---|
| Size | 32.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
df3e866754e1a1238eea12ba3c01e1f1ea65bcf36eb4c2d522537f00ebc38a77
|
|
BLAKE2b-256 checksum How to use checksums |
8c5c45d844c32955366252a07b817859902bcc5b406edcf79b71d1ce35ae6b18
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|