psycopg2
Python
PostgreSQL
database
programming

How do I get a list of column names from a psycopg2 cursor?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When you execute a query with psycopg2, the cursor stores metadata about the result set in cursor.description. That metadata includes column names, which makes it easy to build dictionaries, label exported rows, or inspect dynamic query results without hardcoding the schema in Python.

Read Column Names From cursor.description

After executing a query, each entry in cursor.description describes one output column. The first item in each entry is the column name.

python
1import psycopg2
2
3conn = psycopg2.connect(
4    dbname="demo",
5    user="demo",
6    password="secret",
7    host="localhost",
8)
9
10with conn, conn.cursor() as cur:
11    cur.execute("SELECT id, email, created_at FROM users LIMIT 3")
12    column_names = [desc[0] for desc in cur.description]
13    print(column_names)

That is the standard answer for query-result column names in psycopg2.

Remember That the Query Must Run First

cursor.description is populated by the last executed statement that produced a result set. If you inspect it before executing a SELECT, it will be None.

python
1with conn, conn.cursor() as cur:
2    print(cur.description)  # None
3    cur.execute("SELECT 1 AS value")
4    print([desc[0] for desc in cur.description])

That detail matters when writing helper functions. The column metadata belongs to the current result set, not to the table in the abstract.

Use It to Build Row Dictionaries

A common use case is turning raw tuples into dictionaries keyed by column name.

python
1with conn, conn.cursor() as cur:
2    cur.execute("SELECT id, email FROM users ORDER BY id LIMIT 2")
3    columns = [desc[0] for desc in cur.description]
4    rows = cur.fetchall()
5
6    records = [dict(zip(columns, row)) for row in rows]
7    print(records)

This is handy when the query changes dynamically or when you want lightweight structured output without switching cursor factories.

Consider RealDictCursor for Named Rows

If your main goal is dictionary-like rows, psycopg2.extras.RealDictCursor can avoid the manual zip step.

python
1import psycopg2
2from psycopg2.extras import RealDictCursor
3
4conn = psycopg2.connect(
5    dbname="demo",
6    user="demo",
7    password="secret",
8    host="localhost",
9)
10
11with conn, conn.cursor(cursor_factory=RealDictCursor) as cur:
12    cur.execute("SELECT id, email FROM users LIMIT 2")
13    rows = cur.fetchall()
14    print(rows)

Even when using RealDictCursor, understanding cursor.description is still useful because it explains where those column labels come from.

Query Results, Not Table Definitions

cursor.description reflects the columns of the executed query result, which means aliases and computed expressions appear exactly as selected.

python
with conn, conn.cursor() as cur:
    cur.execute("SELECT id, email AS user_email, NOW() AS fetched_at FROM users LIMIT 1")
    print([desc[0] for desc in cur.description])

This is often better than querying table metadata separately because it tells you what your actual SQL returned, not just what the underlying table schema contains.

Use It Carefully With Non-SELECT Statements

For statements that do not produce a result set, cursor.description stays None.

python
with conn, conn.cursor() as cur:
    cur.execute("UPDATE users SET email = email")
    print(cur.description)  # None

If your code handles arbitrary SQL, check for None before iterating.

Common Pitfalls

  • Reading cursor.description before executing a query that returns rows.
  • Assuming it describes table metadata rather than the current query result.
  • Forgetting that aliases appear exactly as written in the SQL.
  • Treating non-SELECT statements as if they should have column metadata.
  • Reimplementing row-dictionary behavior when a cursor factory might already solve the real need.

Summary

  • In psycopg2, column names come from cursor.description after a result-producing query runs.
  • The first element of each description entry is the output column name.
  • The metadata describes the query result, including aliases, not just the base table.
  • Use it to build dictionaries or inspect dynamic queries.
  • Check for None when the statement may not return rows.

Course illustration
Course illustration

All Rights Reserved.