Python e o alegere bună pentru proiect dintr-un motiv practic: tot ce îți trebuie pentru o aplicație
completă — fereastră, butoane, tabel, bază de date — vine în biblioteca standard. Tkinter pentru
interfață, sqlite3 pentru date. Nu instalezi niciun pachet și nu pornești niciun server.
Pagina asta e despre o aplicație desktop scrisă în cod: o fereastră Tkinter peste o bază SQLite cu cel puțin trei tabele legate. Dacă vrei o bază de date construită vizual, din ferestre, e proiectul în Access. Dacă vrei interfața desenată cu mouse-ul, într-un designer, e proiectul în C# cu Windows Forms. Dacă vrei o aplicație care rulează în browser, e proiectul în PHP și MySQL.
Dacă încă alegi între tehnologii, pornește de la cum îți alegi tema de proiect. Cadrul general al examenului e în ghidul atestatului.
Pentru cine e potrivit
Un proiect în Python merge dacă:
- ai scris deja câteva programe în Python și sintaxa nu te mai încurcă — anul de atestat nu e momentul în care înveți un limbaj de la zero;
- tema are date care se leagă — clienți și mașini, elevi și note, produse și comenzi;
- vrei să scrii tot codul tu, inclusiv interfața, și să-l poți explica rând cu rând.
Nu merge dacă profesorul îndrumător cere explicit altă tehnologie sau dacă tema e în esență un site de prezentare. Pentru a doua variantă e mai potrivit un proiect în HTML, CSS și JavaScript.
Un lucru de știut dinainte: subiectele de programare de la proba practică se rezolvă în C/C++ sau Pascal, deci comisia e obișnuită cu C++. Întrebarea „de ce Python?” e aproape sigură. Un răspuns bun e concret: biblioteca standard îți dă interfață și bază de date fără instalări, iar timpul câștigat a mers în modelul de date. „E mai ușor” nu e un răspuns.
Capcana numărul unu: un script în consolă nu e o aplicație
Cel mai frecvent proiect slab în Python e un program cu input() și print() într-o buclă cu
meniu. Funcționează, dar e o problemă de la proba practică, mai lungă — nu un produs software.
| Nivel | Ce conține | Cum e primit |
|---|---|---|
| Meniu în consolă, date în liste | Se pierde tot la închidere | Insuficient |
| Fereastră Tkinter, date într-un fișier text | Interfață, dar fără model de date | Minim acceptabil |
Fereastră + SQLite cu 3–4 tabele legate, interogări cu JOIN și agregare |
Aplicație reală | Ținta |
| Plus căutare, validări, editare, export | Aplicație închegată | Peste așteptări |
Ținta e rândul al treilea. Peste el treci doar dacă îți rămâne timp după documentație.
Schema: trei tabele, scrise în cod
Exemplul de pe pagina asta e un service auto: clienții au mașini, mașinile au reparații. Două relații unu la mai mulți, deci două chei străine de justificat.
# baza.py
import sqlite3
SCHEMA = """
CREATE TABLE IF NOT EXISTS clienti(
id_client INTEGER PRIMARY KEY,
nume TEXT NOT NULL,
telefon TEXT
);
CREATE TABLE IF NOT EXISTS masini(
id_masina INTEGER PRIMARY KEY,
nr_inmatriculare TEXT NOT NULL UNIQUE,
marca TEXT,
id_client INTEGER NOT NULL REFERENCES clienti(id_client)
);
CREATE TABLE IF NOT EXISTS reparatii(
id_reparatie INTEGER PRIMARY KEY,
id_masina INTEGER NOT NULL REFERENCES masini(id_masina),
data TEXT NOT NULL,
descriere TEXT,
cost REAL CHECK (cost >= 0)
);
"""
def conecteaza(cale="service.db"):
con = sqlite3.connect(cale)
con.execute("PRAGMA foreign_keys = ON") # implicit e OFF, la fiecare conexiune
con.executescript(SCHEMA)
return con
CREATE TABLE IF NOT EXISTS face ca aplicația să-și creeze singură baza la prima pornire. Nu
livrezi un fișier gol separat și nu există pasul „rulează întâi scriptul de creare”.
telefon e TEXT, nu INTEGER. Într-o coloană INTEGER, SQLite transformă "0722000111" în
numărul 722000111 — zeroul de la început dispare fără nicio eroare. Aceeași capcană ca la
subiectul de baze de date de la proba practică.
Capcana pe care nu o vezi: cheile străine sunt oprite implicit
În modulul sqlite3 din Python, cheile străine nu se verifică decât dacă le activezi, la
fiecare conexiune, cu PRAGMA foreign_keys = ON. Fără linia aia, REFERENCES din schemă e doar
decor:
| Ce încerci | Fără PRAGMA |
Cu PRAGMA foreign_keys = ON |
|---|---|---|
O mașină pentru clientul cu id 99, care nu există |
se salvează | FOREIGN KEY constraint failed |
| Ștergi un client care are mașini | se șterge, mașinile rămân orfane | FOREIGN KEY constraint failed |
Asta e exact întrebarea pe care o pune un evaluator care știe ce caută: „ce se întâmplă dacă șterg un client care are mașini?” Cu linia de mai sus ai un răspuns pe care îl poți demonstra pe loc.
CHECK (cost >= 0) funcționează și fără PRAGMA: o reparație cu cost negativ e refuzată cu
CHECK constraint failed. E o validare la nivelul bazei — se apără și dacă interfața greșește.
Interfața: o fereastră care citește și scrie în bază
# aplicatie.py
import tkinter as tk
from tkinter import ttk, messagebox
from baza import conecteaza
class Aplicatie(tk.Tk):
def __init__(self, con):
super().__init__()
self.con = con
self.title("Service auto — clienți")
ttk.Label(self, text="Nume").grid(row=0, column=0, padx=6, pady=4, sticky="w")
self.nume = ttk.Entry(self, width=30)
self.nume.grid(row=0, column=1, padx=6, pady=4)
ttk.Label(self, text="Telefon").grid(row=1, column=0, padx=6, pady=4, sticky="w")
self.telefon = ttk.Entry(self, width=30)
self.telefon.grid(row=1, column=1, padx=6, pady=4)
ttk.Button(self, text="Adaugă client", command=self.adauga).grid(
row=2, column=1, padx=6, pady=4, sticky="e")
self.tabel = ttk.Treeview(self, columns=("nume", "telefon"), show="headings")
self.tabel.heading("nume", text="Nume")
self.tabel.heading("telefon", text="Telefon")
self.tabel.grid(row=3, column=0, columnspan=2, padx=6, pady=6)
self.reincarca()
def adauga(self):
nume = self.nume.get().strip()
if not nume:
messagebox.showwarning("Date lipsă", "Numele clientului e obligatoriu.")
return
with self.con: # commit la final, rollback dacă apare o eroare
self.con.execute(
"INSERT INTO clienti(nume, telefon) VALUES (?, ?)",
(nume, self.telefon.get().strip()),
)
self.nume.delete(0, tk.END)
self.telefon.delete(0, tk.END)
self.reincarca()
def reincarca(self):
self.tabel.delete(*self.tabel.get_children())
for id_client, nume, telefon in self.con.execute(
"SELECT id_client, nume, telefon FROM clienti ORDER BY nume"
):
self.tabel.insert("", tk.END, iid=id_client, values=(nume, telefon))
if __name__ == "__main__":
Aplicatie(conecteaza()).mainloop()
Trei detalii din codul ăsta pe care trebuie să le poți explica:
command=self.adauga, fără paranteze. Îi dai butonului funcția, ca s-o apeleze el la click. Cucommand=self.adauga(), funcția rulează o singură dată, la construirea ferestrei, iar butonul primește rezultatul ei — adică nimic. Butonul apare, dar nu face nimic la click.with self.con:face commit la ieșirea din bloc. Fără el (sau fărăcon.commit()), datele apar în tabel cât timp aplicația e deschisă și dispar la închidere. E cel mai neplăcut tip de bug: în timpul lucrului totul pare corect.iid=id_clientleagă rândul din tabelul vizual de cheia primară. Când adaugi Șterge sau Editează,self.tabel.selection()îți dă direct id-ul din bază, fără să cauți după nume.
reincarca() recitește tabelul din bază după fiecare modificare. E mai simplu și mai sigur decât să
actualizezi manual rândurile din fereastră: ce vezi e întotdeauna ce e în bază.
Interogarea care se notează
Comisia nu se uită la butoane, ci la ce știe aplicația să calculeze. Un raport cu JOIN și
GROUP BY e partea tehnică pe care o poți arăta în treizeci de secunde:
SELECT c.nume, COUNT(r.id_reparatie) AS reparatii, SUM(r.cost) AS total
FROM clienti AS c
JOIN masini AS m ON m.id_client = c.id_client
JOIN reparatii AS r ON r.id_masina = m.id_masina
GROUP BY c.id_client
ORDER BY total DESC;
Pe un client cu două mașini și trei reparații, rezultatul e un singur rând: numele, 3, suma
costurilor. Legătura client → reparație trece prin tabelul masini — exact ce justifici când
explici schema. Tehnicile sunt aceleași ca la
interogările SQL de la proba practică.
Datele utilizatorului nu intră niciodată direct în SQL
Semnul de întrebare din VALUES (?, ?) nu e un detaliu de stil. Comparația, pe aceeași bază cu doi
clienți:
nume = "' OR '1'='1" # ce a scris cineva în câmpul de căutare
con.execute(f"SELECT nume FROM clienti WHERE nume = '{nume}'").fetchall()
# -> ambii clienți: condiția a devenit adevărată pentru toate rândurile
con.execute("SELECT nume FROM clienti WHERE nume = ?", (nume,)).fetchall()
# -> [] textul e căutat ca text, nu executat
Cu f-string, textul introdus devine cod SQL. Cu ?, rămâne o valoare. Se numește SQL
injection și e o întrebare bună de pus în documentație înainte s-o pună comisia. Aceeași regulă,
pentru aplicații web, e tratată pe larg la PHP și MySQL.
Cum ajunge aplicația pe calculatorul comisiei
Pe calculatorul din sala de examen s-ar putea să nu existe Python. Soluția e un executabil:
pip install pyinstaller
pyinstaller --onefile --windowed --name ServiceAuto aplicatie.py
Rezultatul e dist/ServiceAuto.exe — pentru aplicația de pe pagina asta, un singur fișier de
aproximativ 13 MB, care pornește fără Python instalat. --windowed ascunde fereastra neagră de
consolă din spatele aplicației.
Baza service.db se creează în folderul din care pornești executabilul, la prima rulare. Pune
exe-ul într-un folder al lui, cu baza cu date de test lângă el, și copiază folderul întreg.
Testează pe alt calculator cu cel puțin o săptămână înainte. Pe al tău merge întotdeauna. Adu o
copie pe stick, una în cloud, și sursele .py separat — dacă executabilul refuză să pornească,
sursele rulează pe orice calculator cu Python.
Planul pe luni
Termenul de depunere e 1 mai, cu referatul profesorului îndrumător. Înapoi de la data aia:
| Când | Ce faci |
|---|---|
| Decembrie | Alegi tema, o discuți cu profesorul. Entitățile și relațiile pe hârtie |
| Ianuarie | baza.py: schema, cheile străine, 20–30 de înregistrări de test pe tabel |
| Februarie | Fereastra principală: adăugare, listare, ștergere pe primul tabel |
| Martie | Restul tabelelor, căutarea, rapoartele cu JOIN și GROUP BY |
| Aprilie | Executabilul testat pe alt PC, documentația, repetiția prezentării |
| 1 mai | Depunere |
Separă baza de interfață din prima zi — două fișiere, ca mai sus. Când comisia te întreabă unde e logica de date, arăți un fișier, nu cauți prin 400 de rânduri amestecate.
Ce te întreabă comisia
„De ce Python și nu C++, cum ați lucrat la clasă?” Răspunsul concret: interfața și baza de date sunt în biblioteca standard, deci nu depinzi de nimic instalat în plus. Spune și ce ai făcut cu timpul ăsta.
„Ce face mainloop()?”
Pornește bucla de evenimente: fereastra așteaptă click-uri și taste și apelează funcțiile legate de
ele. Codul de după mainloop() rulează abia după ce închizi fereastra.
„Ce se întâmplă dacă șterg un client care are mașini?”
Cu PRAGMA foreign_keys = ON, baza refuză ștergerea. Arată-le pe loc. Dacă ai ales altceva — de
exemplu ON DELETE CASCADE — explică de ce.
„Unde se salvează datele?”
În fișierul service.db, prin commit. Arată-le că datele rămân după ce închizi și redeschizi
aplicația.
„Arată-mi o interogare mai complicată și explică-mi-o.”
Alege dinainte una cu JOIN și GROUP BY, ca cea de mai sus, și repet-o cu voce tare.
„Ce ai face altfel dacă ai lua-o de la capăt?” O limitare reală, spusă concret: lipsa editării, căutarea doar după nume, un singur utilizator.
Cum decurge proba și ce înseamnă motivarea teoretică: prezentarea proiectului în fața comisiei.
Greșeli frecvente
- Meniu în consolă în loc de aplicație. Un program cu
input()într-o buclă e un exercițiu, nu un produs software. - Date ținute în liste. Totul dispare la închidere, iar la prezentare pornești de la zero.
REFERENCESfărăPRAGMA foreign_keys = ON. Schema arată corect, dar baza acceptă orice.commituitat. Datele există doar până închizi aplicația.command=functie()cu paranteze. Butonul nu face nimic la click.- SQL construit cu
f-stringsau cu+. Prima întrebare despre securitate și n-ai răspuns. - Telefon sau CNP ca
INTEGER. Zeroul din față dispare. - Tot codul într-un singur fișier. Greu de explicat și imposibil de arătat pe bucăți.
- Executabilul netestat pe alt calculator. Descoperi problema în sala de examen.
- Bază aproape goală. Trei înregistrări nu produc niciun raport convingător.
Ce livrezi
- Aplicația — executabilul, baza cu date de test și sursele
.py; - Documentația — schema cu relațiile, justificarea tabelelor, capturi ale ferestrelor și explicația interogărilor principale;
- Prezentarea — maximum 30 de minute, cu aplicația rulând.
Pentru partea de SQL, teoria e în baze de date și SQL și în interogări SQL, cu probleme rezolvate din examenele reale.
Întrebări frecvente
Se acceptă Python la proiectul de atestat?
Metodologia nu impune un limbaj pentru proiect — tehnologia o stabilește catedra liceului tău. Întreabă profesorul îndrumător înainte să începi și justifică alegerea în documentație.
Trebuie să instalez ceva în afară de Python?
Pentru aplicație, nu: Tkinter și sqlite3 fac parte din biblioteca standard. Ai nevoie de
PyInstaller doar la final, ca să produci executabilul.
Tkinter sau PyQt?
Tkinter e suficient pentru atestat și vine cu Python. PyQt arată mai modern, dar e o instalare în plus și un strat de complexitate pe care trebuie să-l poți explica. Nota vine din modelul de date, nu din aspectul ferestrei.
De ce nu se verifică cheia străină, deși am scris REFERENCES?
În sqlite3 din Python, cheile străine sunt dezactivate implicit. Rulează
PRAGMA foreign_keys = ON imediat după fiecare sqlite3.connect().
De ce dispar datele după ce închid aplicația?
Lipsește commit. Pune modificările într-un bloc with con: sau apelează con.commit() după ele.
Pot folosi MySQL în loc de SQLite?
Poți, dar MySQL e un server care trebuie instalat și pornit și pe calculatorul comisiei. SQLite e un singur fișier lângă aplicație. Pentru o aplicație desktop, SQLite e alegerea mai sigură.
Câte tabele trebuie să aibă baza de date?
Nu există un număr impus. Sub două tabele nu ai nicio relație de justificat; trei-patru tabele legate corect sunt suficiente pentru orice temă de liceu.
Se poate face proiectul în echipă?
Da, în echipă de 2–3 elevi, în funcție de complexitate. Împărțirea naturală la Python: unul pe
baza.py și rapoarte, altul pe ferestre — fiecare trebuie să poată explica și partea celuilalt.
De unde vin informațiile de aici
Cerința ca proiectul să fie un produs software însoțit de prezentarea și motivarea teoretică, termenul de depunere de 1 mai, referatul profesorului îndrumător și posibilitatea lucrului în echipă de 2–3 elevi sunt din Metodologia de organizare și desfășurare a examenului de atestare a competențelor profesionale ale absolvenților claselor de matematică-informatică și matematică-informatică, intensiv informatică, aprobată prin OMECI nr. 4.843/2009 și modificată prin OM nr. 3.009/2021.
Limbajul, numărul de tabele și nivelul tehnic așteptat nu sunt prevăzute în metodologie — se
stabilesc de catedra de specialitate a liceului tău. Codul de pe pagina asta a fost rulat pe
Python 3.14 cu Tk 9.0, iar comportamentele descrise (cheile străine oprite implicit, datele pierdute
fără commit, zeroul pierdut din telefon, injecția prin f-string, executabilul PyInstaller) au
fost verificate pe același cod. Verifică întotdeauna cerința profesorului îndrumător.