Aufwärmen · Aufgabe 1 von 7
Aufwärmen aus der vorigen Lektion: Was gibt das aus?
import tomllib
limit = tomllib.loads("limit = 10")["limit"]
print(limit + 1, type(limit).__name__)// A5.3 · ca. 35 Min. · Vertiefung
Nach dieser Lektion fragen Sie SQLite aus Python mit ? und benannten Platzhaltern ab, fügen viele Zeilen mit executemany ein, machen Änderungen per Transaktion ganz oder gar nicht und erklären, warum aus Strings gebautes SQL zur SQL-Injection einlädt.
Danach können Sie
Aufwärmen · Aufgabe 1 von 7
import tomllib
limit = tomllib.loads("limit = 10")["limit"]
print(limit + 1, type(limit).__name__)Vorhersagen · Aufgabe 2 von 7
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name, secret)")
con.executemany("INSERT INTO users VALUES (?, ?)", [("ada", "a1"), ("bob", "b2")])
name = "nobody' OR '1'='1" # typed into a search box
sql = f"SELECT name FROM users WHERE name = '{name}'" # the mistake
print(con.execute(sql).fetchall())Üben · Aufgabe 3 von 7
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name, secret)")
con.executemany("INSERT INTO users VALUES (?, ?)", [("ada", "a1"), ("bob", "b2")])
name = "ada"
rows = con.execute("SELECT secret FROM users WHERE name = ?", ____)
print(rows.fetchall())Üben · Aufgabe 4 von 7
import sqlite3
con = sqlite3.connect(":memory:")
row = con.execute("SELECT :a + :b", {"a": 2, "b": 3}).fetchone()
print(row)Üben · Aufgabe 5 von 7
Denksport · Aufgabe 6 von 7
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT UNIQUE)")
try:
with con:
con.execute("INSERT INTO t(name) VALUES (?)", ("a",))
con.execute("INSERT INTO t(name) VALUES (?)", ("a",))
except sqlite3.IntegrityError:
pass
print(con.execute("SELECT count(*) FROM t").fetchone()[0])Anwenden · Aufgabe 7 von 7
Prüfen Sie Ihr Ergebnis anhand dieser Liste
Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.
Ausgearbeitetes Beispiel
Das Programm legt eine Tabelle im Speicher an und fügt mit executemany in with con: drei Bücher ein. by_author() übergibt den Autor als Parameter, ein Injection-Versuch ist also nur ein Name, der nichts findet. Ein benannter Platzhalter füllt :year. Zum Schluss scheitert eine Transaktion mit einem gültigen und einem doppelten Buch, und with con: rollt beide Einfügungen zurück: Die Tabelle enthält weiter drei Bücher.
main.py
import sqlite3
con = sqlite3.connect(":memory:") # a database that lives in memory only
con.execute("CREATE TABLE book(title TEXT UNIQUE, author TEXT, year INTEGER)")
books = [
("Dune", "Frank Herbert", 1965),
("Emma", "Jane Austen", 1815),
("Persuasion", "Jane Austen", 1817),
]
with con: # commits when the block succeeds
con.executemany("INSERT INTO book VALUES (?, ?, ?)", books)
def by_author(author: str) -> list[str]:
rows = con.execute("SELECT title FROM book WHERE author = ? ORDER BY year", (author,))
return [title for (title,) in rows]
print(by_author("Jane Austen"))
print(by_author("x' OR '1'='1")) # just text: no author has this name
query = "SELECT count(*) FROM book WHERE year < :year"
print(con.execute(query, {"year": 1900}).fetchone())
try:
with con: # rolls back when the block raises
con.execute("INSERT INTO book VALUES (?, ?, ?)", ("Ulysses", "James Joyce", 1922))
con.execute("INSERT INTO book VALUES (?, ?, ?)", ("Emma", "Someone Else", 2000))
except sqlite3.IntegrityError as err:
print("rolled back:", err)
print(con.execute("SELECT count(*) FROM book").fetchone()[0], "books")
con.close()
Ausführen mit
python main.pyAusgabe
['Emma', 'Persuasion']
[]
(2,)
rolled back: UNIQUE constraint failed: book.title
3 booksTab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Übung 1 von 2
Beide Funktionen in main.py bauen SQL mit f-Strings. Ein Autor wie O'Brien bringt sie zum Absturz, und eine präparierte Eingabe liefert alle Titel. Schreiben Sie sie mit ?-Platzhaltern neu. search_titles(con, word) muss weiterhin Titel finden, die word irgendwo enthalten, die %-Platzhalterzeichen gehören also jetzt in den gebundenen Wert.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Das SQL wird konstant: "SELECT title FROM book WHERE author = ? ORDER BY title", mit (author,) als zweitem Argument.
Ein ? darf nicht in Anführungszeichen stehen. Schreiben Sie title LIKE ? und binden Sie f"%{word}%".
Denken Sie an das Komma: (author,) ist ein Tupel, (author) nur ein String.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import sqlite3
def find_by_author(con: sqlite3.Connection, author: str) -> list[str]:
"""Titles by exactly this author, in title order."""
sql = "SELECT title FROM book WHERE author = ? ORDER BY title"
return [row[0] for row in con.execute(sql, (author,))]
def search_titles(con: sqlite3.Connection, word: str) -> list[str]:
"""Titles that contain word anywhere, in title order."""
sql = "SELECT title FROM book WHERE title LIKE ? ORDER BY title"
return [row[0] for row in con.execute(sql, (f"%{word}%",))]
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import sqlite3
def find_by_author(con: sqlite3.Connection, author: str) -> list[str]:
"""Titles by exactly this author, in title order."""
sql = f"SELECT title FROM book WHERE author = '{author}' ORDER BY title"
return [row[0] for row in con.execute(sql)]
def search_titles(con: sqlite3.Connection, word: str) -> list[str]:
"""Titles that contain word anywhere, in title order."""
sql = f"SELECT title FROM book WHERE title LIKE '%{word}%' ORDER BY title"
return [row[0] for row in con.execute(sql)]
test_main.py
import sqlite3
from main import find_by_author, search_titles
def make_db():
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE book(title TEXT, author TEXT)")
con.executemany(
"INSERT INTO book VALUES (?, ?)",
[
("Emma", "Jane Austen"),
("Persuasion", "Jane Austen"),
("Dune", "Frank Herbert"),
("At Swim-Two-Birds", "Flann O'Brien"),
],
)
return con
def test_author():
"""find_by_author findet beide Titel von Austen"""
got = find_by_author(make_db(), "Jane Austen")
assert got == ["Emma", "Persuasion"], f"find_by_author lieferte {got!r}"
def test_apostrophe():
"""Ein Name mit Apostroph funktioniert"""
got = find_by_author(make_db(), "Flann O'Brien")
assert got == ["At Swim-Two-Birds"], f"find_by_author lieferte {got!r}"
def test_author_injection():
"""Ein Injection-Versuch ist nur ein Name, der nichts findet"""
got = find_by_author(make_db(), "x' OR '1'='1")
assert got == [], f"find_by_author lieferte {got!r}: binden Sie den Wert mit ?"
def test_search():
"""search_titles findet Titel, die das Wort enthalten"""
got = search_titles(make_db(), "u")
assert got == ["Dune", "Persuasion"], f"search_titles lieferte {got!r}"
def test_search_injection():
"""Anführungszeichen im Suchwort können die Abfrage nicht ändern"""
got = search_titles(make_db(), "' OR 1=1 OR title = '")
assert got == [], f"search_titles lieferte {got!r}: die %-Zeichen gehören in den gebundenen Wert"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenÜbung 2 von 2
transfer(con, src, dst, amount) verschiebt Geld zwischen Zeilen von account(name, balance). Machen Sie es sicher: Führen Sie beide Updates in with con: aus und lösen Sie ValueError aus, wenn src oder dst nicht existiert (prüfen Sie cur.rowcount) oder src unter null fallen würde. Jeder ValueError im Block muss alle Änderungen zurückrollen, und eine gelungene Überweisung muss committet sein.
Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.
Beim ersten Ausführen lädt Ihr Browser Python herunter (bis zu 6.5 MB) und speichert es im Cache. Ihr Code bleibt auf Ihrem Gerät.
Lösen Sie innerhalb des with con:-Blocks aus: Der Rollback geschieht, weil der Block mit einer Ausnahme endet.
cur.rowcount ist die Zahl der Zeilen, die das UPDATE geändert hat; 0 heißt, es gibt kein solches Konto.
Lesen Sie nach dem ersten UPDATE den neuen Kontostand mit einem SELECT und lösen Sie ValueError aus, wenn er unter 0 liegt.
Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.
import sqlite3
def transfer(con: sqlite3.Connection, src: str, dst: str, amount: int) -> None:
"""Move amount from src to dst: both updates happen, or neither does."""
if amount <= 0:
raise ValueError("amount must be positive")
with con: # commit on success, roll back if anything below raises
cur = con.execute("UPDATE account SET balance = balance - ? WHERE name = ?", (amount, src))
if cur.rowcount != 1:
raise ValueError(f"no account {src!r}")
(left,) = con.execute("SELECT balance FROM account WHERE name = ?", (src,)).fetchone()
if left < 0:
raise ValueError(f"{src!r} has too little money")
cur = con.execute("UPDATE account SET balance = balance + ? WHERE name = ?", (amount, dst))
if cur.rowcount != 1:
raise ValueError(f"no account {dst!r}")
Installieren Sie Python 3.14 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.
main.py
import sqlite3
def transfer(con: sqlite3.Connection, src: str, dst: str, amount: int) -> None:
"""Move amount from src to dst: both updates happen, or neither does."""
# Nothing is committed, and nothing stops an overdraft or an unknown account.
con.execute("UPDATE account SET balance = balance - ? WHERE name = ?", (amount, src))
con.execute("UPDATE account SET balance = balance + ? WHERE name = ?", (amount, dst))
test_main.py
import sqlite3
from main import transfer
def make_db():
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE account(name TEXT PRIMARY KEY, balance INTEGER)")
con.executemany("INSERT INTO account VALUES (?, ?)", [("ada", 100), ("bob", 50)])
con.commit()
return con
def balances(con):
return dict(con.execute("SELECT name, balance FROM account"))
def raises_value_error(con, *args):
try:
transfer(con, *args)
except ValueError:
return True
return False
def test_moves_money():
"""30 gehen von ada an bob"""
con = make_db()
transfer(con, "ada", "bob", 30)
got = balances(con)
assert got == {"ada": 70, "bob": 80}, f"die Kontostände sind {got!r}"
def test_committed():
"""Die Überweisung wird committet"""
con = make_db()
transfer(con, "ada", "bob", 30)
assert not con.in_transaction, "eine Transaktion ist noch offen: mit with con: wird sie committet"
def test_overdraft():
"""Zu wenig Geld löst ValueError aus und ändert nichts"""
con = make_db()
raised = raises_value_error(con, "ada", "bob", 500)
got = balances(con)
assert raised and got == {"ada": 100, "bob": 50}, f"ValueError ausgelöst: {raised}, Kontostände {got!r}"
def test_unknown_account():
"""Ein unbekannter Empfänger löst ValueError aus, und ada behält ihr Geld"""
con = make_db()
raised = raises_value_error(con, "ada", "eve", 10)
got = balances(con)
assert raised and got == {"ada": 100, "bob": 50}, f"ValueError ausgelöst: {raised}, Kontostände {got!r}: die erste Änderung zurückrollen"
Unter macOS und Linux tippen Sie python3, wo in diesen Befehlen python steht, wie in der ersten Lektion.
Programm ausführen:
python main.pyPrüfungen ausführen (learnrun.py muss im selben Ordner liegen):
python learnrun.py testlearnrun.py herunterladenimport sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name)")
name = "O'Brien"
con.execute(f"SELECT * FROM users WHERE name = '{name}'")
Was Python ausgibt
sqlite3.OperationalError: near "Brien": syntax errorWarum, und die Lösung
Der Apostroph in O'Brien beendet den SQL-String zu früh, und SQLite liest Brien dann als SQL. Anführungszeichen von Hand zu verdoppeln ist fehleranfällig. Nehmen Sie einen Platzhalter: con.execute("SELECT * FROM users WHERE name = ?", (name,)). Der Wert wird nie Teil des SQL-Textes.
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE users(name)")
con.execute("SELECT * FROM users WHERE name = ?", "ada")
Was Python ausgibt
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 3 supplied.Warum, und die Lösung
parameters muss eine Sequenz von Werten sein, und ein str ist eine Sequenz von Zeichen: "ada" liefert drei Werte. Übergeben Sie ein Tupel, ("ada",), mit Komma, oder eine Liste, ["ada"].
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("SELECT :name", ["ada"])
Was Python ausgibt
sqlite3.ProgrammingError: Binding 1 (':name') is a named parameter, but you supplied a sequence which requires nameless (qmark) placeholders.Warum, und die Lösung
Ein benannter Platzhalter wird über seinen Namen gefunden, er braucht also ein dict: con.execute("SELECT :name", {"name": "ada"}). Listen und Tupel passen nur zu ?-Platzhaltern.
Python im Browser: Pyodide 314.0.7, MPL-2.0. Lizenz und Quellcode
5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.
Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.