A desktop voting machine built with Python (Tkinter) and MySQL, simulating a simplified electronic ballot: candidate selection, null vote, and vote confirmation, with all votes persisted to a relational database.
This was my first project combining Python with MySQL. It has since been refactored from a single-file script into a layered structure (connection / service / UI), and the database was normalized from a flat vote table into a proper candidates + votes relational model with a foreign key.
- 🗳️ Electronic Voting Machine
- Python 3.11+
- Tkinter (GUI)
- MySQL 8+
mysql-connector-pythonpython-dotenv
- Layered architecture: connection handling, business logic, and UI are fully separated (
db/,services/,ui/), instead of one monolithic script. - Safe resource management: database connections and cursors are opened with Python's
withstatement, so they're always closed properly, even if an error occurs mid-query. - Environment-based configuration: database credentials are loaded from a local
senha.envfile (never committed), with an absolute path resolution (Path(__file__).resolve()) so the app works regardless of which directory it's launched from. - Relational normalization (v1 → v2): candidates were originally hardcoded in Python and votes stored only as raw numbers. The schema was redesigned so candidates live in their own table, and
votes.candidate_idis a real foreign key — guaranteeing every non-null vote points to an existing candidate. - Parameterized queries: all
INSERT/SELECTstatements use placeholders (%s), avoiding SQL injection.
urna-eletronica/
├── main.py # Application entry point
├── sql/
│ ├── schema.sql # Table creation + seed data
│ └── select.sql # Useful queries for checking election results
├── db/
│ ├── __init__.py
│ └── connection.py # Database connection handling
├── services/
│ ├── __init__.py
│ └── vote_service.py # Business logic: candidate lookup, vote registration
├── ui/
│ ├── __init__.py
│ └── voting_window.py # Tkinter interface
├── img/ # Screenshots and GIFs
├── requirements.txt
├── .env.example # Template for environment variables (no real credentials)
└── README.md
Run sql/schema.sql against your MySQL server. It will:
- Create the
db_machine_votingdatabase (if it doesn't exist) - Create the
candidatesandvotestables - Seed the three initial candidates
sql/select.sql contains a set of ready-to-use queries for auditing the election: total votes per candidate, null vote count, current winner, vote percentage, votes over time, and a full timestamped audit log.
Example output — total votes per candidate:
| candidate | vote_number | total_votes |
|---|---|---|
| Jampaguara | 1234 | 8 |
| Kendry | 8989 | 5 |
| Shaulin | 4567 | 2 |
Example output — current winner:
| candidate | total_votes |
|---|---|
| Jampaguara | 8 |
Example output — audit log (most recent votes):
| id | candidate | date_hour |
|---|---|---|
| 16 | Null vote | 2026-07-04 14:32:10 |
| 15 | Kendry | 2026-07-04 14:31:48 |
| 14 | Jampaguara | 2026-07-04 14:30:57 |
| Table | Description |
|---|---|
candidates |
Stores each candidate's ballot number and name |
votes |
Stores each cast vote, linked to a candidate via candidate_id (nullable for blank/null votes) |
CREATE TABLE candidates (
id INT NOT NULL AUTO_INCREMENT,
vote_number INT NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE votes (
id INT NOT NULL AUTO_INCREMENT,
candidate_id INT DEFAULT NULL,
date_hour DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
FOREIGN KEY (candidate_id) REFERENCES candidates(id)
);The voter types the candidate's ballot number, confirms the name shown on screen, and the vote is recorded against that candidate.
Pressing Null immediately records a vote with no candidate attached — used when the voter chooses not to vote for anyone.
Before a vote is confirmed, the typed number is looked up in the candidates table. If no candidate matches that ballot number, the vote is rejected with an error message and the field is cleared.
Every valid vote requires an explicit yes/no confirmation dialog showing the candidate's name, preventing accidental votes from a mistyped number.
A null vote is stored with candidate_id = NULL, distinguishing it from a normal vote at the database level, not just in the UI.
- Make sure you have Python 3.11+ and MySQL 8+ installed.
- Install the dependencies:
pip install -r requirements.txt
- Run
sql/schema.sqlagainst your MySQL server to create the database, tables, and seed candidates. - Copy
.env.exampletosenha.envand fill in your actual MySQL credentials:DB_HOST=127.0.0.1 DB_USER=root DB_PASS=your_password_here DB_NAME=db_machine_voting - Run the app:
python main.py
- Build an admin/results screen inside the app itself, using the queries from
sql/select.sqlas a base (currently they need to be run manually). - Add input masking/limits so the entry field only accepts digits.
- Add automated tests for
vote_service.py, since business logic is now fully decoupled from the UI.

