Class 12Computer Science · Database ManagementFull chapter

Python–SQL Connectivity

The whole chapter in one place — read it, then test yourself. Clear notes, a reference sheet, a practice quiz, and worked NCERT solutions & PYQs.

Connecting Python to MySQL

Quick answer MySQL is a server and your Python program is just another client, so every connectivity program is the same chain — import mysql.connector, connect(), cursor(), execute() — and each link breaks with its own recognisable error.

MySQL is a server. It is a program that keeps running in the background and listens on a port — 3306 unless someone changed it. Your data lives inside that server, not inside your .py file. So Python never opens the database files directly. It becomes a client and asks the server, exactly the way the mysql command-line tool does.

The library that speaks MySQL's protocol on Python's behalf is called a connector. The one CBSE names is mysql-connector-python. Install it once from the command prompt:

pip install mysql-connector-python

Note the trap straight away: the package you install is mysql-connector-python, but the name you import is mysql.connector. Two different strings for the same thing.

Every connectivity program, however large, is these five steps:

  1. Import the connector.
  2. connect() — open a line to the server. It returns a connection object.
  3. cursor() — ask the connection for a cursor object. The cursor is what actually carries SQL to the server and holds whatever comes back.
  4. execute() — send one SQL statement.
  5. Fetch the rows (for SELECT) or commit() (for INSERT/UPDATE/DELETE), then close.

Why two objects instead of one? Because the connection is the line and the cursor is the worker. One connection can hand out several cursors, so one program can hold more than one result set open at a time. That split also explains which method sits where: execute() and the fetch methods belong to the cursor, while commit(), rollback() and close() finish work on the whole line and so belong to the connection.

Worked example. Every output printed in this chapter came from a real run against MySQL 8.0.41 with mysql-connector-python on Python 3.13. The demo server was on port 3307; on a normal machine leave port out and it defaults to 3306.

import mysql.connector

con = mysql.connector.connect(host="127.0.0.1", port=3307, user="root",
                              password="", database="cs12_python_sql_connectivity")
print("type(con)      :", type(con).__name__)
print("is_connected() :", con.is_connected())
print("server version :", con.server_info)

cur = con.cursor()
print("type(cur)      :", type(cur).__name__)

cur.execute("SELECT DATABASE()")
print("current db     :", cur.fetchone())

cur.execute("SELECT Name, City, Salary FROM EMPLOYEE WHERE Dept='IT'")
print("column names   :", [d[0] for d in cur.description])
for row in cur.fetchall():
    print(row)

cur.close()
con.close()
print("after close    :", con.is_connected())

Real output:

type(con)      : CMySQLConnection
is_connected() : True
server version : 8.0.41
type(cur)      : CMySQLCursor
current db     : ('cs12_python_sql_connectivity',)
column names   : ['Name', 'City', 'Salary']
('Diya Nair', 'Kochi', Decimal('72000.00'))
('Rohan Verma', 'Pune', Decimal('65000.00'))
after close    : False

Four things to read off that output. First, fetchone() returned a tuple, not a plain value — ('cs12_python_sql_connectivity',) — so to print just the value you index it, cur.fetchone()[0]. Second, a DECIMAL column comes back as a Decimal object, not a float, which is why the salaries print as Decimal('72000.00'). Third, is_connected() flips to False the instant you close.

Fourth, the class names begin with C. The pip wheel ships an optional C extension, and when it is present connect() gives you a CMySQLConnection and a CMySQLCursor. Pass use_pure=True and you get the pure-Python MySQLConnection and MySQLCursor instead — the pair that textbooks quote. Everything in this chapter behaves identically on both except one detail, flagged in Section 2.

The EMPLOYEE table used everywhere in this chapter was itself created from Python — no phpMyAdmin, no command line:

import mysql.connector

con = mysql.connector.connect(host="127.0.0.1", port=3307, user="root", password="")
print("connected:", con.is_connected())
cur = con.cursor()
cur.execute("CREATE DATABASE cs12_python_sql_connectivity")
cur.execute("USE cs12_python_sql_connectivity")
cur.execute("""
CREATE TABLE EMPLOYEE (
  EmpID   INT PRIMARY KEY,
  Name    VARCHAR(25),
  City    VARCHAR(20),
  Dept    VARCHAR(15),
  Salary  DECIMAL(10,2)
)
""")
rows = [
 (101, 'Aarav Sharma',   'Delhi',   'Sales',    58000),
 (102, 'Diya Nair',      'Kochi',   'IT',       72000),
 (103, 'Rohan Verma',    'Pune',    'IT',       65000),
 (104, 'Ishita Banerjee','Kolkata', 'HR',       49000),
 (105, 'Kabir Singh',    'Jaipur',  'Sales',    53000),
 (106, 'Meera Iyer',     'Chennai', 'Accounts', 61000),
]
cur.executemany("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)", rows)
con.commit()
print("rows inserted:", cur.rowcount)
cur.execute("SELECT * FROM EMPLOYEE")
for r in cur.fetchall():
    print(r)
cur.close(); con.close()

Real output — these six rows are the seed table, and every later example says whether it starts from them:

connected: True
rows inserted: 6
(101, 'Aarav Sharma', 'Delhi', 'Sales', Decimal('58000.00'))
(102, 'Diya Nair', 'Kochi', 'IT', Decimal('72000.00'))
(103, 'Rohan Verma', 'Pune', 'IT', Decimal('65000.00'))
(104, 'Ishita Banerjee', 'Kolkata', 'HR', Decimal('49000.00'))
(105, 'Kabir Singh', 'Jaipur', 'Sales', Decimal('53000.00'))
(106, 'Meera Iyer', 'Chennai', 'Accounts', Decimal('61000.00'))

When connect() fails. Put it inside try ... except mysql.connector.Error so the user gets a sentence instead of a traceback. Two failures, both produced on purpose:

import mysql.connector

try:
    con = mysql.connector.connect(host="127.0.0.1", port=3307, user="root",
                                  password="wrongpassword",
                                  database="cs12_python_sql_connectivity")
except mysql.connector.Error as e:
    print("Error class :", type(e).__name__)
    print("errno       :", e.errno)
    print("msg         :", e.msg)

Real output:

Error class : ProgrammingError
errno       : 1045
msg         : Access denied for user 'root'@'localhost' (using password: YES)

Asking for a database that does not exist gives, on the same server, ProgrammingError with errno 1049 and the message Unknown database 'no_such_db_xyz'. Learn to recognise those two numbers. Between them, 1045 (wrong user or password) and 1049 (wrong database name) explain most of the "my program won't connect" problems in a school lab.

Wrong-object mistakes cost marks every single year. All four of these were triggered deliberately, and the messages below are exactly what Python printed on the default connection:

What you wroteWhat Python said
cur.commit()AttributeError: 'CMySQLCursor' object has no attribute 'commit'
con.execute("SELECT 1")AttributeError: 'CMySQLConnection' object has no attribute 'execute'
cur.execute(...) after cur.close()ProgrammingError: 2055: Cursor is not connected
con.cursor() after con.close()OperationalError: MySQL Connection not available.

On a connection made with use_pure=True the same four mistakes give the same four errors with the un-prefixed class names — 'MySQLCursor' and 'MySQLConnection' — which is how most textbooks print them. The mistake is identical either way, so quote whichever your own machine shows.

Compress all four into one sentence and you will never mix them up: execute and fetch belong to the cursor; connect, commit, rollback and close belong to the connection.

Import the driver import mysql.connector The pip package is mysql-connector-python; the import path is mysql.connector. Getting this backwards gives ModuleNotFoundError.
connect() con = mysql.connector.connect(host="localhost", user="root", password="", database="school") port defaults to 3306. The older aliases passwd= and db= also work (both verified). Wrong password raises ProgrammingError with errno 1045; wrong database name raises errno 1049.
Check the link is alive print(con.is_connected()) Returns True while open and False after con.close(). It returns a value — it prints nothing on its own.
cursor() cur = con.cursor() The cursor runs the SQL, not the connection. One connection can create several cursors, each with its own result set.
execute() cur.execute("SELECT * FROM EMPLOYEE") One statement per call. It only sends the query — it does not return the rows. You must still fetch them.
Close in reverse order cur.close(); con.close() Closing the connection without commit() throws away every uncommitted INSERT, UPDATE and DELETE. See Section 3.
Remember
  • MySQL is a server listening on a port (3306 by default); Python is a client and needs the mysql-connector-python driver to reach it.
  • Install it as mysql-connector-python but import it as mysql.connector — the two names differ.
  • connect() returns the connection (the line) and cursor() returns the cursor (the worker). execute() and the fetch methods are cursor methods; commit(), rollback() and close() are connection methods.
  • fetchone() always returns a tuple, so index it with [0]; a DECIMAL column arrives as a Decimal object, not a float.
  • errno 1045 means wrong user or password and errno 1049 means unknown database — catch mysql.connector.Error instead of letting a traceback reach the user.
  • With the C extension installed the objects are CMySQLConnection and CMySQLCursor; use_pure=True gives the MySQLConnection and MySQLCursor that textbooks name. Error messages quote whichever you are using.

Reading Data — fetchone(), fetchall() and rowcount

Quick answer A SELECT leaves its rows in a result set with a position marker, so fetchone() advances that marker, fetchall() returns only what is left, and rowcount is -1 until you actually fetch something.

A SELECT does not hand the rows to Python by itself. execute() only sends the query. The rows sit in a result set attached to the cursor, and the cursor keeps a position marker in it — picture a finger pointing at the next unread row. The three fetch methods differ only in how many rows they take and how far they push that finger.

MethodReturnsMoves the markerWhen rows run out
fetchone()one tupleforward by 1 rowNone
fetchmany(n)list of at most n tuplesforward by n rows[]
fetchall()list of all remaining tuplesto the end[]

The word remaining is where marks are lost. fetchall() does not rewind to the top. Whatever you already fetched is gone.

Worked example. The EMPLOYEE table holds the six seed rows created in Section 1. This one program demonstrates the whole section:

import mysql.connector

con = mysql.connector.connect(host="127.0.0.1", port=3307, user="root",
                              password="", database="cs12_python_sql_connectivity",
                              use_pure=True)
cur = con.cursor()
print("cursor class :", type(cur).__name__)

print("rowcount before execute() :", cur.rowcount)
cur.execute("SELECT EmpID, Name, Salary FROM EMPLOYEE ORDER BY EmpID")
print("rowcount after  execute() :", cur.rowcount)

print("fetchone() #1 ->", cur.fetchone())
print("fetchone() #2 ->", cur.fetchone())
print("rowcount now              :", cur.rowcount)

rest = cur.fetchall()
print("fetchall() returns", len(rest), "rows:")
for r in rest:
    print("   ", r)
print("rowcount after fetchall   :", cur.rowcount)
print("fetchone() past the end   :", cur.fetchone())
print("fetchall() past the end   :", cur.fetchall())
cur.close()
con.close()

Real output:

cursor class : MySQLCursor
rowcount before execute() : -1
rowcount after  execute() : -1
fetchone() #1 -> (101, 'Aarav Sharma', Decimal('58000.00'))
fetchone() #2 -> (102, 'Diya Nair', Decimal('72000.00'))
rowcount now              : 2
fetchall() returns 4 rows:
    (103, 'Rohan Verma', Decimal('65000.00'))
    (104, 'Ishita Banerjee', Decimal('49000.00'))
    (105, 'Kabir Singh', Decimal('53000.00'))
    (106, 'Meera Iyer', Decimal('61000.00'))
rowcount after fetchall   : 6
fetchone() past the end   : None
fetchall() past the end   : []

Walk through the four lessons buried in that output.

  • fetchone() moves the marker. The two calls returned different rows — 101 and then 102. It is not a "get first row" function; it is a "get next row" function.
  • fetchall() returns only what is left. The table has six rows but len(rest) is 4, because 101 and 102 had already been taken. This is the single most common trap in this chapter.
  • rowcount starts at -1. Straight after execute() it was still -1. It became 2 after two fetchone() calls and 6 after fetchall(). So on a plain cursor, rowcount reports rows fetched so far, not rows matched. Print it before fetching and you get -1, not the answer you wanted.
  • Running off the end is not an error. fetchone() gives None and fetchall() gives []. That is why the standard idiom is if rec: after a fetchone and if not rows: after a fetchall.

An honest note on -1, and why the program above sets use_pure=True. This driver ships two cursor implementations, as Section 1 explained. The pure-Python one prints -1 after execute() and is the one every textbook describes; the optional C-extension cursor printed 0 in the same place on the same machine. Both agree on the real point: the number is meaningless until you fetch. In the board exam, -1 is the expected answer. In your own programs, do not depend on either — count with len(cur.fetchall()).

The C-extension cursor has one further quirk worth knowing before your practical: after you mix fetchone() and fetchall() on the same result set, a further fetchone() raises InternalError: No result set available instead of returning None. On its own result set it returns None correctly, as the standard rule says. If you want to demonstrate the past-the-end rule, run it on a fresh query and nothing surprising happens.

If you genuinely need the count first, ask for a buffered cursor. It pulls the entire result set from the server immediately, so rowcount is correct right away:

cur = con.cursor(buffered=True)
cur.execute("SELECT * FROM EMPLOYEE WHERE Dept='Sales'")
print("buffered cursor, rowcount straight after execute():", cur.rowcount)
print("rows:", cur.fetchall())

Real output:

buffered cursor, rowcount straight after execute(): 2
rows: [(101, 'Aarav Sharma', 'Delhi', 'Sales', Decimal('58000.00')), (105, 'Kabir Singh', 'Jaipur', 'Sales', Decimal('53000.00'))]

fetchmany(n) sits between the two and is useful when a table is too large to print at once:

cur.execute("SELECT EmpID, Name FROM EMPLOYEE ORDER BY EmpID")
print(cur.fetchmany(3))
print(cur.fetchone())

Real output — note that the fetchone() afterwards gives row 104, not row 101, because fetchmany(3) already consumed the first three:

[(101, 'Aarav Sharma'), (102, 'Diya Nair'), (103, 'Rohan Verma')]
(104, 'Ishita Banerjee')

Ask for more rows than are left and you simply get fewer, with no error. On this six-row table three successive fetchmany(4) calls returned 4 rows, then 2 rows, then [].

Leave rows unread and the next execute() fails. On a plain (unbuffered) cursor the server is still holding rows for you, so a fresh query on the same cursor is refused:

cur.execute("SELECT * FROM EMPLOYEE")
cur.fetchone()
cur.execute("SELECT COUNT(*) FROM EMPLOYEE")

Real output — identical on both cursor implementations:

InternalError : Unread result found

The fix is to finish the result set — call cur.fetchall() and ignore it, or use buffered=True. If this error ever appears in your practical exam, it means you fetched one row and moved on.

Column headings. After any SELECT, cur.description holds one entry per column and item 0 of each is the column name, so you can print headings without hard-coding them. On the Section 1 query it gave:

column names   : ['Name', 'City', 'Salary']

Before any execute(), and after a statement that returns no rows such as an UPDATE, cur.description is None — both checked on a live cursor.

fetchone() rec = cur.fetchone() One tuple, then the marker moves forward. Returns None once the rows are exhausted. Index it: rec[0], rec[1] ...
fetchall() data = cur.fetchall() A LIST of tuples — only the rows still unread. Empty result set gives [], never None.
fetchmany(n) cur.fetchmany(3) List of at most n rows; fewer near the end, [] once empty. It advances the marker exactly like fetchone does.
rowcount print(cur.rowcount) -1 on a plain cursor until you fetch (0 on the C-extension cursor); afterwards it is the number of rows fetched so far. On INSERT/UPDATE/DELETE it is immediately the rows affected.
Buffered cursor cur = con.cursor(buffered=True) Pulls the whole result set at once, so rowcount is correct straight after execute() and 'Unread result found' cannot happen.
Column headings from the cursor [d[0] for d in cur.description] cur.description is available only after a SELECT; each entry describes one column and item 0 is its name.
Remember
  • execute() only sends the query; the rows wait in the cursor's result set until you fetch them.
  • fetchone() returns the NEXT row as a tuple and advances the marker, so two calls give two different rows; fetchall() then returns only the rows still left.
  • On a plain cursor rowcount is -1 until you fetch, and afterwards counts rows fetched so far — use len(cur.fetchall()) when you want a count you can trust, or cursor(buffered=True) to get rowcount right after execute().
  • Running past the end is not an error: fetchone() gives None and fetchall() gives [] — hence the idioms if rec: and if not rows:.
  • Fetching one row and then reusing the same unbuffered cursor raises InternalError: Unread result found; finish the result set first.
  • cur.description is None before any execute() and after a non-SELECT; after a SELECT, item 0 of each entry is the column name.

INSERT, UPDATE, DELETE and Why commit() Decides Everything

Quick answer DML runs through the same execute() and reports rows affected in rowcount, but the connector opens every session with autocommit off, so a write that is never committed is silently thrown away when the connection closes.

INSERT, UPDATE and DELETE go through exactly the same cur.execute() as a SELECT. Two things change. There are no rows to fetch, and rowcount is immediately useful — for a write it is the number of rows the statement affected, available at once, with no fetching involved.

Worked example. Starting from the six seed rows of Section 1:

cur.execute("INSERT INTO EMPLOYEE (EmpID,Name,City,Dept,Salary) VALUES (%s,%s,%s,%s,%s)",
            (107, "Ananya Rao", "Hyderabad", "IT", 68000))
print("INSERT rowcount :", cur.rowcount)
con.commit()

cur.execute("UPDATE EMPLOYEE SET Salary = Salary + 5000 WHERE Dept=%s", ("Sales",))
print("UPDATE rowcount :", cur.rowcount)
con.commit()

cur.execute("UPDATE EMPLOYEE SET Salary = 99999 WHERE City=%s", ("Shimla",))
print("UPDATE (no match) rowcount :", cur.rowcount)
con.commit()

cur.execute("DELETE FROM EMPLOYEE WHERE Salary < %s", (50000,))
print("DELETE rowcount :", cur.rowcount)
con.commit()

cur.execute("SELECT * FROM EMPLOYEE ORDER BY EmpID")
for r in cur.fetchall():
    print("   ", r)

Real output:

INSERT rowcount : 1
UPDATE rowcount : 2
UPDATE (no match) rowcount : 0
DELETE rowcount : 1
    (101, 'Aarav Sharma', 'Delhi', 'Sales', Decimal('63000.00'))
    (102, 'Diya Nair', 'Kochi', 'IT', Decimal('72000.00'))
    (103, 'Rohan Verma', 'Pune', 'IT', Decimal('65000.00'))
    (105, 'Kabir Singh', 'Jaipur', 'Sales', Decimal('58000.00'))
    (106, 'Meera Iyer', 'Chennai', 'Accounts', Decimal('61000.00'))
    (107, 'Ananya Rao', 'Hyderabad', 'IT', Decimal('68000.00'))

Read the 0 carefully. The Shimla update did not fail and did not raise anything — it simply matched nothing. That is why a real application must print cur.rowcount after every update and delete. Telling a user "record updated" when rowcount was 0 is a lie your program told on your behalf.

One refinement, checked on the same table: for an UPDATE this driver reports rows changed, not rows matched. Running UPDATE EMPLOYEE SET Salary=Salary WHERE Dept='Sales' matches two rows but alters no value, and rowcount came back 0. So rowcount 0 after an update means "nothing actually changed", which may be because nothing matched or because the new value equalled the old one.

To insert many rows in one call, use executemany() with a list of tuples:

cur.executemany("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)",
                [(108, "Vikram Joshi", "Nagpur",    "HR",       47000),
                 (109, "Sneha Pillai", "Bengaluru", "Accounts", 59000)])
print("executemany rowcount :", cur.rowcount)
con.commit()
cur.execute("SELECT COUNT(*) FROM EMPLOYEE")
print("total rows now       :", cur.fetchone()[0])
executemany rowcount : 2
total rows now       : 8

Now the part that decides marks: commit(). Here is the experiment, starting again from the six seed rows, run twice, changing exactly one line.

import mysql.connector

CFG = dict(host="127.0.0.1", port=3307, user="root", password="",
           database="cs12_python_sql_connectivity", use_pure=True)

def count():
    c = mysql.connector.connect(**CFG)
    k = c.cursor()
    k.execute("SELECT COUNT(*) FROM EMPLOYEE")
    n = k.fetchone()[0]
    k.close(); c.close()
    return n

def row(i):
    c = mysql.connector.connect(**CFG)
    k = c.cursor()
    k.execute("SELECT * FROM EMPLOYEE WHERE EmpID=%s", (i,))
    v = k.fetchall()
    k.close(); c.close()
    return v

print("rows at start :", count())

# RUN 1 : no commit()
con = mysql.connector.connect(**CFG)
cur = con.cursor()
cur.execute("INSERT INTO EMPLOYEE VALUES (107,'Ananya Rao','Hyderabad','IT',68000)")
print("RUN 1 (no commit) -- rowcount after INSERT :", cur.rowcount)
cur.execute("SELECT * FROM EMPLOYEE WHERE EmpID=107")
print("RUN 1 -- same connection sees it :", cur.fetchall())
cur.close()
con.close()                      # closed WITHOUT commit()
print("RUN 1 -- rows after reconnecting :", count())
print("RUN 1 -- row 107 after reconnect :", row(107))

# RUN 2 : identical, plus one line
con = mysql.connector.connect(**CFG)
cur = con.cursor()
cur.execute("INSERT INTO EMPLOYEE VALUES (107,'Ananya Rao','Hyderabad','IT',68000)")
print("RUN 2 (with commit) -- rowcount :", cur.rowcount)
con.commit()
cur.close()
con.close()
print("RUN 2 -- rows after reconnecting :", count())
print("RUN 2 -- row 107 after reconnect :", row(107))

Real output:

rows at start : 6
RUN 1 (no commit) -- rowcount after INSERT : 1
RUN 1 -- same connection sees it : [(107, 'Ananya Rao', 'Hyderabad', 'IT', Decimal('68000.00'))]
RUN 1 -- rows after reconnecting : 6
RUN 1 -- row 107 after reconnect : []
RUN 2 (with commit) -- rowcount : 1
RUN 2 -- rows after reconnecting : 7
RUN 2 -- row 107 after reconnect : [(107, 'Ananya Rao', 'Hyderabad', 'IT', Decimal('68000.00'))]

Look at Run 1 line by line, because this is exactly how students get fooled in the lab. rowcount said 1. A SELECT on the same connection found the row. Every signal said success. Then the connection closed, a new one opened, and the count was back to 6 and row 107 returned []. The insert was thrown away.

Why? Because the table is InnoDB, a transactional engine, and the session the connector opened has autocommit switched off. A separate check confirmed it precisely — the MySQL server's own global setting is 1, but the session is 0:

con = mysql.connector.connect(**CFG)
cur = con.cursor()
cur.execute("SHOW TABLE STATUS LIKE 'EMPLOYEE'")
print("engine :", cur.fetchone()[1])
cur.execute("SELECT @@GLOBAL.autocommit, @@SESSION.autocommit")
print("global / session autocommit on server :", cur.fetchone())
print("con.autocommit (connector property)   :", con.autocommit)
engine : InnoDB
global / session autocommit on server : (1, 0)
con.autocommit (connector property)   : False

So the driver deliberately switches autocommit off for you. Every DML statement you run starts a transaction, and a transaction that is not committed when the connection ends is rolled back. That is the safe default — it means a program that crashes halfway through updating ten records leaves the table untouched rather than half-changed. But it also means con.commit() is not optional decoration. It is the line that saves your work.

One-line rule for the exam: every INSERT, UPDATE or DELETE must be followed by con.commit(); SELECT never needs it.

rollback() is the same power used on purpose. Wrap several writes in a try, and if any one of them fails, undo all of them. Again from the six seed rows:

con = mysql.connector.connect(**CFG)
cur = con.cursor()
print("rows before :", count())
try:
    cur.execute("INSERT INTO EMPLOYEE VALUES (110,'Farhan Qureshi','Lucknow','IT',57000)")
    print("first INSERT rowcount :", cur.rowcount)
    cur.execute("INSERT INTO EMPLOYEE VALUES (101,'Duplicate Id','Delhi','HR',40000)")
    con.commit()
    print("both committed")
except mysql.connector.Error as e:
    con.rollback()
    print("error errno", e.errno, ":", e.msg)
    print("rolled back")
con.close()
print("rows after  :", count())
print("row 110     :", row(110))

Real output:

rows before : 6
first INSERT rowcount : 1
error errno 1062 : Duplicate entry '101' for key 'employee.PRIMARY'
rolled back
rows after  : 6
row 110     : []

The first insert genuinely succeeded, but because the second violated the primary key, rollback() removed both. The table is left exactly as it was. Errno 1062 is the duplicate-primary-key error and is worth memorising alongside 1045 and 1049. Its exception class is IntegrityError, which is a subclass of mysql.connector.Error, so the single except above catches it.

Turning autocommit on is possible but you rarely want it, because you lose rollback:

con = mysql.connector.connect(autocommit=True, **CFG)
cur = con.cursor()
cur.execute("SELECT @@SESSION.autocommit")
print("autocommit=True -> session autocommit :", cur.fetchone()[0])
cur.execute("INSERT INTO EMPLOYEE VALUES (111,'Nikhil Rane','Nashik','HR',45000)")
cur.close(); con.close()          # again NO commit() call
print("row 111 after reconnect               :", row(111))
autocommit=True -> session autocommit : 1
row 111 after reconnect               : [(111, 'Nikhil Rane', 'Nashik', 'HR', Decimal('45000.00'))]

Same code, no commit(), and this time the row survived — proof that the earlier disappearance was the transaction, not a bug. Still, write your board answers the normal way, with autocommit left alone and an explicit con.commit().

INSERT with placeholders cur.execute("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)", (107,"Ananya Rao","Hyderabad","IT",68000)) One %s per column, values as a tuple in the same order. Never put quotes around %s.
UPDATE cur.execute("UPDATE EMPLOYEE SET Salary=Salary+5000 WHERE Dept=%s", ("Sales",)) Omit the WHERE and you update every row — there is no undo once you commit.
DELETE cur.execute("DELETE FROM EMPLOYEE WHERE EmpID=%s", (104,)) Check cur.rowcount afterwards: 0 means no such record, so tell the user that rather than 'deleted'.
Save the work con.commit() A method of the CONNECTION. cur.commit() raises AttributeError: 'CMySQLCursor' object has no attribute 'commit' (or 'MySQLCursor' with use_pure=True). Without it the write vanishes when the connection closes.
Undo the work con.rollback() Cancels every uncommitted statement on that connection. Put it in the except block of a try around a group of related writes.
executemany() cur.executemany("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)", rows) rows must be a list of tuples. rowcount then holds the total inserted (2 in the run above). Still needs commit().
Remember
  • INSERT, UPDATE and DELETE use the same cur.execute(); there is nothing to fetch, and cur.rowcount immediately gives the number of rows affected.
  • rowcount 0 after a write means nothing actually changed — the WHERE matched nothing, or (on UPDATE) the new value equalled the old one. That is not an error, so report it instead of claiming success.
  • The connector opens every session with autocommit = 0 even though the server's global setting is 1, so an uncommitted write is rolled back when the connection closes — verified: insert, reconnect, row gone, count back to 6.
  • con.commit() saves the transaction and con.rollback() undoes it; both belong to the connection, never to the cursor.
  • executemany() takes a list of tuples and reports the total in rowcount, but it still needs a commit(). Duplicate primary key raises errno 1062 (an IntegrityError, caught by except mysql.connector.Error).

Passing Values into Queries — %s and format()

Quick answer The syllabus names two ways of putting a Python value into a query, and both are examinable: format() builds the finished SQL text yourself, while %s hands the values to the driver as a separate tuple so it can quote and escape them.

The syllabus names two ways of putting a Python value into a query — the %s format specifier, and format(). Both are examinable and both earn marks. They are not equivalent, though, and this section shows exactly what separates them. Every run below starts from the six seed rows plus (107, 'Ananya Rao', 'Hyderabad', 'IT', 68000).

Way one: format(). Here you build the finished SQL as an ordinary Python string, and execute() receives one completed statement:

dept = "IT"
sal = 60000
q = "SELECT Name, Salary FROM EMPLOYEE WHERE Dept='{}' AND Salary>{}".format(dept, sal)
print("query :", q)
cur.execute(q)
print("rows  :", cur.fetchall())

Real output:

query : SELECT Name, Salary FROM EMPLOYEE WHERE Dept='IT' AND Salary>60000
rows  : [('Diya Nair', Decimal('72000.00')), ('Rohan Verma', Decimal('65000.00')), ('Ananya Rao', Decimal('68000.00'))]

Look hard at the quotes, because this is where the marks go. With format() you are writing the SQL, so you must type the quotes around a text value — '{}' for Dept — and leave them off a number — {} for Salary. Forget the quotes on the text one and the server reads Dept=IT, treats IT as a column name, and raises error 1054.

Way two: %s with a tuple. You write %s wherever a value goes and pass the values as a second argument to execute(). The driver, not you, inserts them:

dept = "IT"
limit = 60000
cur.execute("SELECT Name, City, Salary FROM EMPLOYEE WHERE Dept=%s AND Salary>%s",
            (dept, limit))
for r in cur.fetchall():
    print("   ", r)

Real output:

('Diya Nair', 'Kochi', Decimal('72000.00'))
('Rohan Verma', 'Pune', Decimal('65000.00'))
('Ananya Rao', 'Hyderabad', Decimal('68000.00'))

Now the quote rule is the exact opposite, and mixing the two rules up is the commonest error in this chapter. With %s you never type quotes, because the driver adds its own. Note too that %s is used for every data type. Not %d for the salary, not %f — this is not Python's % operator, and the driver works out the correct SQL literal from the Python type. A number stays unquoted, a string gets quoted, and a datetime.date is formatted as a DATE (checked: it also comes back as a datetime.date).

Three mistakes worth failing on purpose. All three messages below are real:

cur.execute("SELECT Name FROM EMPLOYEE WHERE City=%s", ("Kochi"))
ProgrammingError : Could not process parameters: str(Kochi), it must be of type list, tuple or dict

There is no comma, so ("Kochi") is just a string in brackets, not a tuple. A one-value tuple needs the trailing comma: ("Kochi",). That single comma is a classic one-mark question.

cur.execute("SELECT Name FROM EMPLOYEE WHERE City='%s'", ("Kochi",))
ProgrammingError : 1064 (42000): You have an error in your SQL syntax; ... near 'Kochi''' at line 1

Here the placeholder was quoted by hand. The driver adds its own quotes, so the server received City=''Kochi''. Rule: never put quotes around %s. The driver's job is to quote.

cur.execute("SELECT Name FROM EMPLOYEE WHERE City=%s" % "Kochi")
ProgrammingError : 1054 (42S22): Unknown column 'Kochi' in 'where clause'

That third one is Python's own % operator, not the driver's placeholder. It pasted Kochi in with no quotes at all, so MySQL looked for a column of that name.

Where format() actually breaks: the apostrophe. Consider an Indian name that contains one:

cur.execute("INSERT INTO EMPLOYEE VALUES (109,%s,%s,%s,%s)",
            ("Rani D'Souza", "Panaji", "Sales", 51000))
con.commit()
cur.execute("SELECT * FROM EMPLOYEE WHERE EmpID=109")
print("   apostrophe inserted safely:", cur.fetchone())

name = "Rani D'Souza"
bad = "SELECT * FROM EMPLOYEE WHERE Name='{}'".format(name)
print("   built with format():", bad)
cur.execute(bad)

Real output:

   apostrophe inserted safely: (109, "Rani D'Souza", 'Panaji', 'Sales', Decimal('51000.00'))
   built with format(): SELECT * FROM EMPLOYEE WHERE Name='Rani D'Souza'
   -> ProgrammingError : 1064 (42000): You have an error in your SQL syntax; ... near 'Souza'' at line 1

The same value through %s works:

cur.execute("SELECT * FROM EMPLOYEE WHERE Name=%s", (name,))
print(cur.fetchall())
[(109, "Rani D'Souza", 'Panaji', 'Sales', Decimal('51000.00'))]

The apostrophe closed the string early and broke the query built by format(). The %s version escaped it and worked. Hold on to that observation, because it is the important idea in miniature: a character typed by a user changed the structure of the SQL. With format() the values and the SQL keywords are all one piece of text by the time the server sees them; with %s the driver keeps them apart.

Why that matters beyond apostrophes. Here is a login check written both ways against a two-row LOGIN table (aarav/clerk and principal/admin):

def login_unsafe(u, p):
    q = "SELECT UserID, Role FROM LOGIN WHERE UserID='" + u + "' AND Pwd='" + p + "'"
    print("   SQL sent :", q)
    cur.execute(q)
    return cur.fetchall()

def login_safe(u, p):
    cur.execute("SELECT UserID, Role FROM LOGIN WHERE UserID=%s AND Pwd=%s", (u, p))
    return cur.fetchall()

Real output for four attempts:

A) honest login, unsafe version
   SQL sent : SELECT UserID, Role FROM LOGIN WHERE UserID='aarav' AND Pwd='delhi@123'
   result   : [('aarav', 'clerk')]

B) wrong password, unsafe version
   SQL sent : SELECT UserID, Role FROM LOGIN WHERE UserID='aarav' AND Pwd='guess'
   result   : []

C) attacker types password: x' OR '1'='1
   SQL sent : SELECT UserID, Role FROM LOGIN WHERE UserID='aarav' AND Pwd='x' OR '1'='1'
   result   : [('aarav', 'clerk'), ('principal', 'admin')]

D) same attack against the %s version
   result   : []
   rowcount : 0

Read line C. The visitor typed x' OR '1'='1 into a password box. Their apostrophe closed the Pwd literal, their OR '1'='1' became part of the WHERE clause, and the condition is true for every row. The function returned both accounts, including the admin. No password was ever guessed. The quotes happen to balance, which is exactly why no error was raised to warn anybody.

Line D is the same input through execute(..., (u, p)). The driver treated the whole thing as one string value to compare against the Pwd column, found no user whose password is literally x' OR '1'='1, and returned []. Same input, same table, different result — because the value never reached the SQL parser.

The same trick on a DELETE destroys data. This was run on a fresh copy of the two-row table:

typed = "x' OR '1'='1"
q = "DELETE FROM LOGIN WHERE UserID='" + typed + "'"
print("SQL sent     :", q)
cur.execute(q)
print("rowcount     :", cur.rowcount)
con.commit()

Real output:

LOGIN before : [('aarav', 'clerk'), ('principal', 'admin')]
SQL sent     : DELETE FROM LOGIN WHERE UserID='x' OR '1'='1'
rowcount     : 2
LOGIN after  : []

One row was meant to be deleted at most; both were. Run the identical string through a placeholder instead — cur.execute("DELETE FROM LOGIN WHERE UserID=%s", (typed,)) — and rowcount is 0 and both rows survive. Verified both ways.

So state the principle plainly, and write it this way in the exam: a query's structure must come from the programmer and its values must come from the parameters. Use format() when the question asks for it or when the values are your own constants; use %s whenever the value came from input(), a file or a form.

Two practical footnotes. Named placeholders are available if a query has many parameters and you want the code readable — pass a dictionary instead of a tuple:

cur.execute("SELECT Name, Salary FROM EMPLOYEE WHERE Dept=%(d)s AND Salary>=%(s)s",
            {"d": "IT", "s": 60000})
print(cur.fetchall())
[('Diya Nair', Decimal('72000.00')), ('Rohan Verma', Decimal('65000.00')), ('Ananya Rao', Decimal('68000.00'))]

And placeholders substitute values only. A table name or a column name cannot be a %s parameter — cur.execute("SELECT * FROM %s", (tbl,)) raises ProgrammingError 1064 ... near ''EMPLOYEE'', because the driver quoted the table name as though it were text. If your program must choose a column at run time, pick it from a fixed list you wrote yourself — never from whatever the user typed.

format() — the first syllabus-named way q = "SELECT * FROM EMPLOYEE WHERE Dept='{}' AND Salary>{}".format(dept, sal) cur.execute(q) You write the SQL, so you type the quotes: '{}' for text, {} for a number. Verified working. It does no escaping, so keep it for your own constants, not for user input.
%s with a tuple cur.execute("SELECT * FROM EMPLOYEE WHERE Dept=%s AND Salary>%s", (dept, sal)) One %s per value, in order, whatever the data type. The values go in the SECOND argument of execute(), never inside the query string.
Single parameter cur.execute("SELECT Name FROM EMPLOYEE WHERE City=%s", (city,)) The trailing comma makes it a tuple. Without it you get ProgrammingError: Could not process parameters: str(Kochi).
Never quote the placeholder WHERE City=%s (not WHERE City='%s') Quoting it yourself produced ProgrammingError 1064 near 'Kochi''' — the driver had already supplied the quotes. This is the opposite of the format() rule.
Named placeholders with a dict cur.execute("SELECT Name FROM EMPLOYEE WHERE Dept=%(d)s AND Salary>=%(s)s", {"d":"IT", "s":60000}) Same protection as %s, easier to read when a query has many parameters. Keys must match the names exactly.
What NOT to do with user input q = "SELECT * FROM LOGIN WHERE UserID='" + u + "' AND Pwd='" + p + "'" Runs fine on honest input, which is the trap. Verified: p = "x' OR '1'='1" returned every row of LOGIN, and the same string in a DELETE removed both rows (rowcount 2). Through %s: [] and rowcount 0.
Placeholders carry values, not identifiers cur.execute("SELECT * FROM %s", (tbl,)) -- does not work Verified: raises ProgrammingError 1064 near ''EMPLOYEE''. If a table or column name must vary, choose it from a hard-coded list in your own code.
Remember
  • The syllabus names both ways. With format() you type the quotes yourself — '{}' for a text value, {} for a number; with %s you never type quotes, because the driver supplies them.
  • %s is used for every data type — not %d or %f — because it is a driver placeholder, not Python's % operator; "...City=%s" % "Kochi" pastes the value in unquoted and raises 1054 Unknown column 'Kochi'.
  • A single parameter still needs a tuple with a trailing comma, ("Kochi",); ("Kochi") raises ProgrammingError: Could not process parameters.
  • Never quote the placeholder: WHERE City='%s' produced error 1064 near 'Kochi''' because the driver adds its own quotes.
  • Passing values as a tuple lets the driver escape them — an apostrophe in "Rani D'Souza" broke a query built with format() but stored and matched correctly through %s.
  • Values pasted into the query text are how SQL injection happens: the password x' OR '1'='1 returned every LOGIN row including the admin, and the same string in a DELETE removed both rows (rowcount 2). Through %s the same input returned [] and deleted nothing (rowcount 0).

The formula sheet

Every formula in this chapter, in one place — screenshot it before your exam.

import mysql.connector
Import the driver
con = mysql.connector.connect(host="localhost", user="root", password="", database="school")
connect()
print(con.is_connected())
Check the link is alive
cur = con.cursor()
cursor()
cur.execute("SELECT * FROM EMPLOYEE")
execute()
cur.close(); con.close()
Close in reverse order
rec = cur.fetchone()
fetchone()
data = cur.fetchall()
fetchall()
cur.fetchmany(3)
fetchmany(n)
print(cur.rowcount)
rowcount
cur = con.cursor(buffered=True)
Buffered cursor
[d[0] for d in cur.description]
Column headings from the cursor
cur.execute("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)", (107,"Ananya Rao","Hyderabad","IT",68000))
INSERT with placeholders
cur.execute("UPDATE EMPLOYEE SET Salary=Salary+5000 WHERE Dept=%s", ("Sales",))
UPDATE
cur.execute("DELETE FROM EMPLOYEE WHERE EmpID=%s", (104,))
DELETE
con.commit()
Save the work
con.rollback()
Undo the work
cur.executemany("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)", rows)
executemany()
q = "SELECT * FROM EMPLOYEE WHERE Dept='{}' AND Salary>{}".format(dept, sal) cur.execute(q)
format() — the first syllabus-named way
cur.execute("SELECT * FROM EMPLOYEE WHERE Dept=%s AND Salary>%s", (dept, sal))
%s with a tuple
cur.execute("SELECT Name FROM EMPLOYEE WHERE City=%s", (city,))
Single parameter
WHERE City=%s (not WHERE City='%s')
Never quote the placeholder
cur.execute("SELECT Name FROM EMPLOYEE WHERE Dept=%(d)s AND Salary>=%(s)s", {"d":"IT", "s":60000})
Named placeholders with a dict
q = "SELECT * FROM LOGIN WHERE UserID='" + u + "' AND Pwd='" + p + "'"
What NOT to do with user input
cur.execute("SELECT * FROM %s", (tbl,)) -- does not work
Placeholders carry values, not identifiers
def get_connection(): return mysql.connector.connect(host="localhost", user="root", password="", database="school")
Reusable connection function
while True: ch = input("Choice: ") if ch == "1": add_student(con) elif ch == "6": con.close(); break
Menu loop
cur.execute("INSERT INTO STUDENT VALUES (%s,%s,%s,%s)", (rno, name, cls, marks)) con.commit()
Add a record
rows = cur.fetchall() if not rows: print("No records.")
Display all records
cur.execute("SELECT * FROM STUDENT WHERE RollNo=%s", (rno,)) rec = cur.fetchone()
Search one record
print("Rows updated:", cur.rowcount)
Report what actually happened

Test yourself

Tap an answer to check it instantly — you'll see why it's right, and what to revise if it isn't.

0 correct · 0/12 answered
Q1

Table EMPLOYEE holds 6 rows (EmpID 101 to 106). What does this print? cur.execute("SELECT * FROM EMPLOYEE ORDER BY EmpID") cur.fetchone() cur.fetchone() print(len(cur.fetchall()))

Q2

Two employees are in the IT department. On a plain unbuffered pure-Python cursor (the connection was made with use_pure=True), what does this print? cur.execute("SELECT Name FROM EMPLOYEE WHERE Dept='IT'") print(cur.rowcount)

Q3

EMPLOYEE has 6 rows. Program A connects, runs INSERT INTO EMPLOYEE VALUES (107,...), prints cur.rowcount, then calls con.close() without commit(). Program B then reconnects and runs SELECT COUNT(*) FROM EMPLOYEE. What does Program B print?

Q4

No employee lives in Shimla. What does this print? cur.execute("SELECT * FROM EMPLOYEE WHERE City=%s", ("Shimla",)) print(cur.fetchall())

Q5

EMPLOYEE holds rows 101 to 106 in order. What does this print? cur.execute("SELECT EmpID, Name FROM EMPLOYEE ORDER BY EmpID") cur.fetchmany(3) print(cur.fetchone())

Q6

Two employees are in the Sales department, on 58000.00 and 53000.00. What does this print? cur.execute("UPDATE EMPLOYEE SET Salary=Salary*1.1 WHERE Dept='Sales'") print(cur.rowcount)

Q7

The two IT salaries are 72000.00 and 65000.00 in a DECIMAL(10,2) column. What does this print? cur.execute("SELECT SUM(Salary) FROM EMPLOYEE WHERE Dept='IT'") print(cur.fetchone())

Q8

Exactly one employee has EmpID 102. What does this print? cur.execute("SELECT Name FROM EMPLOYEE WHERE EmpID=102") cur.fetchone() print(cur.fetchone())

Q9

Which call correctly passes a single parameter to execute()?

Q10

What happens when this runs? cur.execute("SELECT Name FROM EMPLOYEE WHERE City='%s'", ("Kochi",))

Q11

A login screen builds its query as q = "SELECT UserID, Role FROM LOGIN WHERE UserID='" + u + "' AND Pwd='" + p + "'". The LOGIN table holds ('aarav','clerk') and ('principal','admin'). A visitor enters user aarav and password x' OR '1'='1 . What does cur.fetchall() return?

Q12

Which statement makes the changes made by an INSERT permanent in the database?

NCERT solutions & previous-year questions

Step-by-step model answers — tap a question to reveal the full solution.

NCERT questions 6

1 What is a cursor? Why is a cursor object needed to work with a MySQL database from a Python program?Cursor object

A cursor is an object created from a connection that acts as the control structure between your Python program and the MySQL server. It carries an SQL statement to the server, and for a SELECT it holds the returned result set together with a position marker showing which row will be read next.

It is needed because the connection alone cannot run SQL. The connection is only the communication line; asking it to execute a statement fails:

con.execute("SELECT 1")
AttributeError: 'CMySQLConnection' object has no attribute 'execute'

(The class is named MySQLConnection, without the leading C, if the connection was made with use_pure=True. Either way the message is the same mistake.)

The cursor is also what makes row-by-row reading possible. Because it remembers a position, fetchone() can return the next row each time it is called, and one connection can create several cursors so that more than one result set is open at once.

con = mysql.connector.connect(host="127.0.0.1", port=3307, user="root",
                              password="", database="cs12_python_sql_connectivity")
cur = con.cursor()
cur.execute("SELECT Name, City FROM EMPLOYEE WHERE Dept='IT'")
print(cur.fetchall())

Real output:

[('Diya Nair', 'Kochi'), ('Rohan Verma', 'Pune')]
2 Differentiate between fetchone() and fetchall(). Illustrate the difference with a program and its output.Fetch methods
fetchone()fetchall()
Returns a single record as a tuple.Returns all remaining records as a list of tuples.
Moves the cursor forward by one row.Moves the cursor to the end of the result set.
Returns None when no row is left.Returns [] when no row is left.
Used to read one record, e.g. a search by roll number.Used to display a whole table.

The key point is that they share one position marker. Whatever fetchone() has already taken, fetchall() will not return again:

cur.execute("SELECT EmpID, Name, Salary FROM EMPLOYEE ORDER BY EmpID")
print("fetchone() #1 ->", cur.fetchone())
print("fetchone() #2 ->", cur.fetchone())
rest = cur.fetchall()
print("fetchall() returns", len(rest), "rows:")
for r in rest:
    print("   ", r)

cur.execute("SELECT Name FROM EMPLOYEE WHERE EmpID=102")
print("only row            ->", cur.fetchone())
print("fetchone() past end ->", cur.fetchone())
cur.execute("SELECT Name FROM EMPLOYEE WHERE City='Shimla'")
print("fetchall(), no match->", cur.fetchall())

Real output on a table of six records:

fetchone() #1 -> (101, 'Aarav Sharma', Decimal('58000.00'))
fetchone() #2 -> (102, 'Diya Nair', Decimal('72000.00'))
fetchall() returns 4 rows:
    (103, 'Rohan Verma', Decimal('65000.00'))
    (104, 'Ishita Banerjee', Decimal('49000.00'))
    (105, 'Kabir Singh', Decimal('53000.00'))
    (106, 'Meera Iyer', Decimal('61000.00'))
only row            -> ('Diya Nair',)
fetchone() past end -> None
fetchall(), no match-> []

Two things to take away. The table had 6 rows but fetchall() gave only 4, because the first two had already been fetched. And reading past the end is not an error: fetchone() returned None and fetchall() returned [], which is why the two methods need different tests — if rec: after a fetchone, if not rows: after a fetchall.

3 What is the significance of the commit() method? What happens to an INSERT statement if commit() is not called before the connection is closed?Transactions and commit()

commit() is a method of the connection object. It ends the current transaction and writes every pending INSERT, UPDATE and DELETE permanently into the database. Until it is called, those changes exist only inside the transaction of that one connection.

The reason it is compulsory is that the connector opens each session with autocommit switched off. Checked on a live server:

cur.execute("SELECT @@GLOBAL.autocommit, @@SESSION.autocommit")
print(cur.fetchone())
print(con.autocommit)
(1, 0)
False

The server's global setting is 1, but the session the driver opened is 0. So an uncommitted transaction is rolled back when the connection ends.

Demonstration — the same insert run twice on a six-row table, differing only in one line. count() and row() are helpers that open a fresh connection each time, which is the whole point:

# RUN 1 : no commit()
con = mysql.connector.connect(**CFG)
cur = con.cursor()
cur.execute("INSERT INTO EMPLOYEE VALUES (107,'Ananya Rao','Hyderabad','IT',68000)")
print("RUN 1 (no commit) -- rowcount after INSERT :", cur.rowcount)
cur.execute("SELECT * FROM EMPLOYEE WHERE EmpID=107")
print("RUN 1 -- same connection sees it :", cur.fetchall())
cur.close()
con.close()                      # closed WITHOUT commit()
print("RUN 1 -- rows after reconnecting :", count())
print("RUN 1 -- row 107 after reconnect :", row(107))

Real output, with Run 2 being the identical code plus con.commit(), the two runs shown together:

rows at start : 6
RUN 1 (no commit) -- rowcount after INSERT : 1
RUN 1 -- same connection sees it : [(107, 'Ananya Rao', 'Hyderabad', 'IT', Decimal('68000.00'))]
RUN 1 -- rows after reconnecting : 6
RUN 1 -- row 107 after reconnect : []
RUN 2 (with commit) -- rowcount : 1
RUN 2 -- rows after reconnecting : 7
RUN 2 -- row 107 after reconnect : [(107, 'Ananya Rao', 'Hyderabad', 'IT', Decimal('68000.00'))]

Without commit() the row was gone after reconnecting even though rowcount had reported 1 and the program's own SELECT had found it. With commit() the row survived. The counterpart method is con.rollback(), which deliberately cancels the pending changes. SELECT statements never need a commit.

4 Write a Python program that connects to a MySQL database and displays all the records of the table EMPLOYEE, along with the number of records displayed.Connectivity — displaying records
import mysql.connector

try:
    con = mysql.connector.connect(host="localhost", user="root",
                                  password="", database="cs12_python_sql_connectivity")
    cur = con.cursor()
    cur.execute("SELECT * FROM EMPLOYEE ORDER BY EmpID")
    data = cur.fetchall()
    if not data:
        print("Table is empty.")
    else:
        for rec in data:
            print(rec)
        print("Number of records :", len(data))
    cur.close()
    con.close()
except mysql.connector.Error as e:
    print("Database error", e.errno, ":", e.msg)

Real output (run on the six-record table, with the demo server's port supplied):

(101, 'Aarav Sharma', 'Delhi', 'Sales', Decimal('58000.00'))
(102, 'Diya Nair', 'Kochi', 'IT', Decimal('72000.00'))
(103, 'Rohan Verma', 'Pune', 'IT', Decimal('65000.00'))
(104, 'Ishita Banerjee', 'Kolkata', 'HR', Decimal('49000.00'))
(105, 'Kabir Singh', 'Jaipur', 'Sales', Decimal('53000.00'))
(106, 'Meera Iyer', 'Chennai', 'Accounts', Decimal('61000.00'))
Number of records : 6

Two points worth the marks. The count comes from len(data) rather than cur.rowcount, because on a plain cursor rowcount is -1 until rows are fetched. And no commit() appears, because nothing was changed — a SELECT never needs one.

5 Write Python code that accepts EmpID, Name, City, Dept and Salary from the user and inserts the record into the table EMPLOYEE. Use the %s format specifier.INSERT with placeholders
import mysql.connector

con = mysql.connector.connect(host="localhost", user="root",
                              password="", database="cs12_python_sql_connectivity")
cur = con.cursor()

eid = int(input("EmpID  : "))
name = input("Name   : ")
city = input("City   : ")
dept = input("Dept   : ")
sal  = float(input("Salary : "))

cur.execute("INSERT INTO EMPLOYEE (EmpID, Name, City, Dept, Salary) VALUES (%s,%s,%s,%s,%s)",
            (eid, name, city, dept, sal))
con.commit()
print("Records inserted :", cur.rowcount)

cur.close()
con.close()

Real output when run with the values 107 / Ananya Rao / Hyderabad / IT / 68000:

Records inserted : 1

Four things the examiner looks for:

  • %s is used for every column, including the numeric ones — it is a driver placeholder, not Python's % operator, so %d and %f are wrong here.
  • The values are passed as a tuple in the second argument of execute(), never joined into the query text. This lets the driver quote and escape them, so a name such as Rani D'Souza is stored correctly.
  • Do not write quotes around the placeholders. VALUES ('%s', ...) gives error 1064.
  • con.commit() is essential — without it the record is discarded when the connection closes.
6 Differentiate between execute() and executemany(). State what rowcount holds after each.execute() vs executemany()
execute()executemany()
Runs one SQL statement.Runs the same statement once for each set of values.
Second argument is one tuple (or dictionary) of values, or is omitted.Second argument is a list of tuples — one tuple per row.
Used for SELECT and for single-row writes.Used only for writes, typically bulk INSERT. It cannot be used to fetch rows.
rowcount is -1 on a SELECT until rows are fetched; on a write it is the rows affected by that one statement.rowcount is the total number of rows affected across all the value sets.

Run on the six-row seed table:

cur.execute("INSERT INTO EMPLOYEE (EmpID,Name,City,Dept,Salary) VALUES (%s,%s,%s,%s,%s)",
            (107, "Ananya Rao", "Hyderabad", "IT", 68000))
print("execute rowcount     :", cur.rowcount)

cur.executemany("INSERT INTO EMPLOYEE VALUES (%s,%s,%s,%s,%s)",
                [(108, "Vikram Joshi", "Nagpur",    "HR",       47000),
                 (109, "Sneha Pillai", "Bengaluru", "Accounts", 59000)])
print("executemany rowcount :", cur.rowcount)
con.commit()

cur.execute("SELECT COUNT(*) FROM EMPLOYEE")
print("total rows now       :", cur.fetchone()[0])

Real output:

execute rowcount     : 1
executemany rowcount : 2
total rows now       : 9

Six seed rows plus one plus two makes nine. Both statements still require con.commit(). Also note that a write statement that matches nothing is not an error — an UPDATE ... WHERE City='Shimla' on this table printed rowcount 0, which a good program reports to the user rather than treating as success.

Previous-year board questions 4

Q1 (1 mark) Name the method that is used to make the changes made by a Python program permanent in a MySQL database. State what happens if this method is not used. CBSE 2021 pattern

Method: commit(), used on the connection object, as con.commit().

If it is not used: the INSERT, UPDATE or DELETE remains part of an open transaction and is rolled back when the connection is closed, so the database is left unchanged. This happens because the connector opens each session with autocommit switched off.

Verified: after inserting a row into a six-row table and closing the connection without commit(), a reconnect still counted 6 rows and a query for the new EmpID returned [], even though cur.rowcount had reported 1.

Common one-mark trap: writing cur.commit(). That raises AttributeError: 'CMySQLCursor' object has no attribute 'commit' ('MySQLCursor' on a use_pure=True connection) — commit belongs to the connection, not the cursor.

Q2 (2 marks) Consider the table EMPLOYEE with the following records: 101 Aarav Sharma Delhi Sales 58000.00 102 Diya Nair Kochi IT 72000.00 103 Rohan Verma Pune IT 65000.00 104 Ishita Banerjee Kolkata HR 49000.00 105 Kabir Singh Jaipur Sales 53000.00 106 Meera Iyer Chennai Accounts 61000.00 Predict the output of the following code, assuming the connection is open and cur is a cursor object: cur.execute("SELECT EmpID, Name FROM EMPLOYEE ORDER BY EmpID") print(cur.fetchone()) print(cur.fetchmany(2)) print(len(cur.fetchall())) CBSE 2023 pattern

Output

(101, 'Aarav Sharma')
[(102, 'Diya Nair'), (103, 'Rohan Verma')]
3

Explanation. The three fetch methods share one position marker in the result set:

  • fetchone() takes row 101 and returns it as a tuple.
  • fetchmany(2) takes the next two rows, 102 and 103, and returns them as a list of tuples.
  • fetchall() returns only what is left — rows 104, 105 and 106 — so len(...) is 3, not 6.

This sequence was run and gave exactly those three lines. The marker's movement was confirmed separately too: fetchmany(3) on this table returned rows 101 to 103 and the fetchone() after it returned (104, 'Ishita Banerjee').

If print(cur.rowcount) were added immediately after execute(), the answer would be -1, since on a plain pure-Python cursor rowcount is unavailable until rows are fetched. (On the optional C-extension cursor the same line prints 0 — verified — so never quote rowcount as a row count before fetching.)

Q3 (3 marks) Write a Python function display_dept() that accepts a department name from the user and displays all the records of table EMPLOYEE belonging to that department, along with the number of records found. Assume the database employee_db on localhost with user root and no password. Use a parameterised query. CBSE 2024 pattern
import mysql.connector

def display_dept():
    con = mysql.connector.connect(host="localhost", user="root",
                                  password="", database="employee_db")
    cur = con.cursor()
    d = input("Enter department : ")
    cur.execute("SELECT EmpID, Name, City, Salary FROM EMPLOYEE WHERE Dept=%s", (d,))
    data = cur.fetchall()
    if not data:
        print("No employee found in", d)
    else:
        for rec in data:
            print(rec)
        print("Number of records :", len(data))
        print("cursor.rowcount   :", cur.rowcount)
    cur.close()
    con.close()

display_dept()

Real output when IT was supplied:

Enter department : IT
(102, 'Diya Nair', 'Kochi', Decimal('72000.00'))
(103, 'Rohan Verma', 'Pune', Decimal('65000.00'))
Number of records : 2
cursor.rowcount   : 2

Marking points. One mark for a correct connect() plus cursor(); one for the parameterised execute() with the value passed as the tuple (d,) — note the trailing comma, without which the driver raises ProgrammingError: Could not process parameters; one for fetching and counting correctly.

Do not write cur.execute("... WHERE Dept='" + d + "'"). It works on ordinary input but hands the user control of your SQL, and it breaks outright on a value containing an apostrophe. Note also that cur.rowcount printed 2 here only because fetchall() had already run; len(data) is the reliable count, and the rowcount line above is included only to show that.

Q4 (5 marks) A company stores staff details in table EMPLOYEE (EmpID, Name, City, Dept, Salary) in database employee_db. Write a Python program that: (i) increases the salary of every employee of a city entered by the user by 10%, and reports how many records were changed; (ii) then deletes all employees earning less than 55000 and reports how many were removed; (iii) finally displays the remaining records. Ensure the changes are saved permanently. CBSE 2025 pattern
import mysql.connector

con = mysql.connector.connect(host="localhost", user="root",
                              password="", database="employee_db")
cur = con.cursor()

try:
    # (i) 10 percent increment for one city
    city = input("Enter city : ")
    cur.execute("UPDATE EMPLOYEE SET Salary = Salary + Salary*0.10 WHERE City=%s", (city,))
    print("Records updated :", cur.rowcount)

    # (ii) remove low earners
    cur.execute("DELETE FROM EMPLOYEE WHERE Salary < %s", (55000,))
    print("Records deleted :", cur.rowcount)

    con.commit()

    # (iii) show what is left
    cur.execute("SELECT EmpID, Name, Salary FROM EMPLOYEE ORDER BY EmpID")
    for rec in cur.fetchall():
        print(rec)

except mysql.connector.Error as e:
    con.rollback()
    print("Error", e.errno, ":", e.msg)

finally:
    cur.close()
    con.close()

Real output, with Kochi entered, on the six-record seed table:

Enter city : Kochi
Records updated : 1
Records deleted : 2
(101, 'Aarav Sharma', Decimal('58000.00'))
(102, 'Diya Nair', Decimal('79200.00'))
(103, 'Rohan Verma', Decimal('65000.00'))
(106, 'Meera Iyer', Decimal('61000.00'))

Diya Nair is the only employee in Kochi, so her salary rose from 72000.00 to 79200.00 — note that the result is rounded back to two decimal places by the DECIMAL(10,2) column. The two employees below 55000 — Ishita Banerjee on 49000 and Kabir Singh on 53000 — were removed, which is why Records deleted is 2.

Marking points. Correct connect() and cursor(); both DML statements parameterised with %s; cur.rowcount printed after each write; a single con.commit() covering both changes; and the final SELECT read with fetchall().

The try / except / finally is what turns this into a full-mark answer. Because both writes share one transaction, an error in the delete rolls back the update as well, so the table can never be left half-changed. Verified separately: after a deliberate duplicate-key failure (errno 1062), con.rollback() removed the insert that had already succeeded and the row count returned to its original value.

Part of Priodemy for School

Interactive CBSE lessons, Class 8–12 — free with every school on Priodemy EduSuite. Explore more chapters and labs on the Priodemy for School hub.

Ask AI