information_schema.columns should provide you with the column data-type info.

For example, given this DDL:

create table foo
(
  id serial,
  name text,
  val int
);


insert into foo (name, val) values ('narf', 1), ('poit', 2);

And this query (filtering out the meta tables to get at your tables):

select *
from information_schema.columns
where table_schema NOT IN ('information_schema', 'pg_catalog')
order by table_schema, table_name;

Will yield 4 rows, for the table foo -- the three columns I defined, plus a FK.

SQL fiddle

Regarding psycopg2, the information_schema-related code that you have shown looks like it should work... What's the entirety of the code? I would also recommend trying to step through the code in a debugger (the built-in pdb is OK, but I would recommend pudb, as it's more full featured and easier to use, but still terminal-based. It only runs on *nix platforms, though, due to the underlying modules it uses.

Edit:

I was able to get the data_type info from information_schema using psycopg2 with the following code:

#!/usr/bin/env python

import psycopg2
import psycopg2.extras

conn = psycopg2.connect("host=<host> dbname=<dbname> user=<user> password=<password>")
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)

cur.execute("""select *
               from information_schema.columns
               where table_schema NOT IN ('information_schema', 'pg_catalog')
               order by table_schema, table_name""")

for row in cur:
    print "schema: {schema}, table: {table}, column: {col}, type: {type}".format(
        schema = row['table_schema'], table = row['table_name'],
        col = row['column_name'], type = row['data_type'])

I prefer to use DictCursors, as I find them much easier to work with, but it should work with a regular cursor, too -- you would just need to change how you accessed the rows.

Also, regarding cur.description, that returns a tuple of tuples. If you want to get at the type_code there, you can do so like this:

print cur.description[0][1]

Where the first dimension in the index of the column you want to look at, and the second dimension is the datum within that column. type_code is always 1. So you could iterate over the outer tuple and always look at its second item, for example.

Answer from khampson on Stack Overflow
🌐
GeeksforGeeks
geeksforgeeks.org › python › get-column-name-and-column-type-with-python-psycopg2
Get Column name and Column type with Python psycopg2 - GeeksforGeeks
July 23, 2025 - After reconnecting, it retrieves the column names and data types for the created table by querying the information_schema.columns, and then prints the results. Finally, it closes the cursor and connection again.
Top answer
1 of 2
6

information_schema.columns should provide you with the column data-type info.

For example, given this DDL:

create table foo
(
  id serial,
  name text,
  val int
);


insert into foo (name, val) values ('narf', 1), ('poit', 2);

And this query (filtering out the meta tables to get at your tables):

select *
from information_schema.columns
where table_schema NOT IN ('information_schema', 'pg_catalog')
order by table_schema, table_name;

Will yield 4 rows, for the table foo -- the three columns I defined, plus a FK.

SQL fiddle

Regarding psycopg2, the information_schema-related code that you have shown looks like it should work... What's the entirety of the code? I would also recommend trying to step through the code in a debugger (the built-in pdb is OK, but I would recommend pudb, as it's more full featured and easier to use, but still terminal-based. It only runs on *nix platforms, though, due to the underlying modules it uses.

Edit:

I was able to get the data_type info from information_schema using psycopg2 with the following code:

#!/usr/bin/env python

import psycopg2
import psycopg2.extras

conn = psycopg2.connect("host=<host> dbname=<dbname> user=<user> password=<password>")
cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor)

cur.execute("""select *
               from information_schema.columns
               where table_schema NOT IN ('information_schema', 'pg_catalog')
               order by table_schema, table_name""")

for row in cur:
    print "schema: {schema}, table: {table}, column: {col}, type: {type}".format(
        schema = row['table_schema'], table = row['table_name'],
        col = row['column_name'], type = row['data_type'])

I prefer to use DictCursors, as I find them much easier to work with, but it should work with a regular cursor, too -- you would just need to change how you accessed the rows.

Also, regarding cur.description, that returns a tuple of tuples. If you want to get at the type_code there, you can do so like this:

print cur.description[0][1]

Where the first dimension in the index of the column you want to look at, and the second dimension is the datum within that column. type_code is always 1. So you could iterate over the outer tuple and always look at its second item, for example.

2 of 2
1
select oid,typname from pg_type;

 oid  |   typname
------+-------------
   16 | bool
   23 | int4
   25 | text
 1043 | varchar
 1184 | timestamptz 
Discussions

postgresql - Get column name and column type in the same query in the correct order using psycopg2 - Database Administrators Stack Exchange
I have a postgresql database and I am querying it using psycopg2 and I need to get the data in the correct order as in table I have found different ways to get the column names from table and column types of the table with 2 different queries. More on dba.stackexchange.com
🌐 dba.stackexchange.com
python - How do I get a list of column names from a psycopg2 cursor? - Stack Overflow
I would like a general way to generate column labels directly from the selected column names, and recall seeing that python's psycopg2 module supports this feature. More on stackoverflow.com
🌐 stackoverflow.com
prepared statement - Get PostgreSQL resultset column types without executing query using psycopg2 - Stack Overflow
Given an arbitrary PostgreSQL query, e.g. SELECT * FROM (...) AS T, how can I get resultset column names and types WITHOUT actually executing the query using psycopg2 Python3 library? I saw JDBC More on stackoverflow.com
🌐 stackoverflow.com
August 3, 2019
python - How to get column attributes from a query using PostgreSQL? - Stack Overflow
I need to get the fields attributes from a query, like in this question:How to get column attributes query from table name using PostgreSQL? but for a query, is there a way of doing this? ... Yes psycopg2 database driver. More on stackoverflow.com
🌐 stackoverflow.com
October 5, 2014
🌐
Psycopg
psycopg.org › docs › usage.html
Basic module usage — Psycopg 2.9.12 documentation
In Python 2 you must register a typecaster in order to receive unicode objects: >>> psycopg2.extensions.register_type(psycopg2.extensions.UNICODE, cur) >>> cur.execute("SELECT data FROM test WHERE num = 74") >>> x = cur.fetchone()[0] >>> print(x, type(x), repr(x)) àèìòù€ <type 'unicode'> u'\xe0\xe8\xec\xf2\xf9\u20ac'
🌐
GitHub
github.com › psycopg › psycopg2 › blob › master › psycopg › column_type.c
psycopg2/psycopg/column_type.c at master · psycopg/psycopg2
{"__getstate__", (PyCFunction)column_getstate, METH_NOARGS }, {"__setstate__", (PyCFunction)column_setstate, METH_O }, {NULL} }; · · PyTypeObject columnType = { PyVarObject_HEAD_INIT(NULL, 0) "psycopg2.extensions.Column", sizeof(columnObject), 0, (destructor)column_dealloc, /* tp_dealloc */ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ (reprfunc)column_repr, /*tp_repr*/ 0, /*tp_as_number*/ &column_sequence, /*tp_as_sequence*/ &column_mapping, /*tp_as_mapping*/ 0
Author   psycopg
Find elsewhere
Top answer
1 of 5
39

Information schema vs. system catalogs

The information schema serves certain purposes. System catalogs are the actual source of truth in Postgres.

The information schema provides standardized views which help with portability - mostly across major Postgres versions as portability across different RDBMS platforms typically is an illusion once your queries are sophisticated enough to look up system catalogs. Notably, major RDBMS like Oracle, SQLite, IBM Db2, SAP ASE, or IBM Informix still don't support the information schema (as of June 2026).

Views in the information schema must jump through many hoops to achieve a format complying to the standard. This makes them slow, sometimes very slow. Compare plans and performance for these basic objects:

EXPLAIN ANALYZE SELECT * from information_schema.columns;
EXPLAIN ANALYZE SELECT * from pg_catalog.pg_attribute;

The difference is remarkable.

Your example

For your example SELECT * from tbl compare the two queries below for this simple table:

CREATE TEMP TABLE foo(
   a numeric(12,3)
 , b timestamp(0)
);

Using pg_attribute:

SELECT attname, format_type(atttypid, atttypmod) AS type
FROM   pg_attribute
WHERE  attrelid = 'foo'::regclass
AND    attnum > 0
AND    NOT attisdropped
ORDER  BY attnum;

format_type() returns the complete type with all modifiers:

attname | type
--------+-------------------------------
a       | numeric(12,3)
b       | timestamp(0) without time zone

Also note that the cast to regclass resolves the table name according to the current search_path. It raises an exception if the name is not valid. See:

  • ERROR: could not find array type for datatype information_schema.sql_identifier

Using information_schema.columns:

SELECT column_name, data_type
FROM   information_schema.columns
WHERE  table_name = 'foo'
ORDER  BY ordinal_position;

The information is standardized, but incomplete:

column_name | data_type
------------+----------------------------
a           | numeric
b           | timestamp without time zone

To get full information for the data type you need to consider all of these columns additionally:

character_maximum_length
character_octet_length
numeric_precision
numeric_precision_radix
numeric_scale
datetime_precision
interval_type
interval_precision

Related answers:

  • How to check if a table exists in a given schema
  • List all columns for a specified table

List of pros & cons

The biggest pros (IMO) in bold.

Information schema views

  • often simpler (depends)
  • slow
  • preprocessed, which may or may not suit your needs
  • selective (users only see objects they have privileges for)
  • conforming to an SQL standard (that's implemented by some of the major RDBMS)
  • mostly portable across major Postgres versions
  • do not require much specific knowledge about Postgres
  • identifiers are descriptive, long and sometimes awkward

System catalogs

  • often more complex (depends), closer to the source
  • fast
  • complete (system columns like oid included)
  • not complying to an SQL standard
  • less portable across major Postgres versions (but basics aren't going to change)
  • require more specific knowledge about Postgres
  • identifiers are terse, less descriptive but conveniently short

Arbitrary query

To get the same list of column names and types from a query, you could use a simple trick: CREATE a temporary table from the query output, then use the same techniques as above.

You can append LIMIT 0, since you do not need actual data:

CREATE TEMP TABLE tmp123 AS
SELECT 1::numeric, now()
LIMIT  0;

To get the data type of individual columns, you can also use the function pg_typeof():

SELECT pg_typeof(1);
2 of 5
4

You can use the psql command line client.

\dt will show a list of tables

\dv will show a list of views

\d [object_name] will describe the schema of the table or view

Not sure how you would describe a query though.

More info: https://manikandanmv.wordpress.com/tag/basic-psql-commands/

🌐
pg_tileserv
access.crunchydata.com › documentation › psycopg2 › 2.7.3 › extras.html
psycopg2.extras - Miscellaneous goodies for Psycopg 2
A row object that allow by-column-name access to data. class psycopg2.extras. RealDictCursor ( *args , **kwargs ) A cursor that uses a real dict as the base type for rows.
Top answer
1 of 4
17

The theory yes, though you could find it very complex indeed.

  • Every table (select * from pg_class) has columns.
  • Every column (select * from pg_attribute) optionally has a "typmod" number.
  • For types with typmod (select * from pg_type) there will be a "typmodout" function.
  • Running the typmod out function on a typmod number will return a string that can be concatenated with the type name to form the kind of user-readable signature you're used to (select 'numeric' || numerictypmodout(786441)) (select geography_typmod_out(1107460))

But, hey, psql generates the strings you want, if we look at what SQL it generates, maybe the answer is in there.

Sure enough, there is a magic function that takes a typeid and typmod and returns the magic string.

select a.attname, format_type(a.atttypid, a.atttypmod) from pg_attribute a where attname = 'geog';

With a join to pg_class you should be able to get this info per-table.

2 of 4
9

With Paul Ramsey help I made it this way:

SELECT a.attname as column_name, format_type(a.atttypid, a.atttypmod) AS data_type
FROM pg_attribute a
JOIN pg_class b ON (a.attrelid = b.relfilenode)
WHERE b.relname = 'my_table_name' and a.attstattarget = -1;

UPDATE

Meanwhile I have created a function to ask for a certain column data type

CREATE OR REPLACE FUNCTION "vsr_get_data_type"(_t regclass, _c text)
  RETURNS text AS
$body$
DECLARE
    _schema text;
    _table text;
    data_type text;
BEGIN
-- Prepare names to use in index and trigger names
IF _t::text LIKE '%.%' THEN
    _schema := regexp_replace (split_part(_t::text, '.', 1),'"','','g');
    _table := regexp_replace (split_part(_t::text, '.', 2),'"','','g');
    ELSE
        _schema := 'public';
        _table := regexp_replace(_t::text,'"','','g');
    END IF;

    data_type := 
    (
        SELECT format_type(a.atttypid, a.atttypmod)
        FROM pg_attribute a 
        JOIN pg_class b ON (a.attrelid = b.oid)
        JOIN pg_namespace c ON (c.oid = b.relnamespace)
        WHERE
            b.relname = _table AND
            c.nspname = _schema AND
            a.attname = _c
     );

    RETURN data_type;
END
$body$ LANGUAGE plpgsql;

The usage is:

SELECT vsr_get_data_type('schema_name.table_name','column_name')
Top answer
1 of 2
8

If you want the Python type classes, like you might get from a SQLALchemy column object, you'll need to build and maintain your own mapping. psycopg2 doesn't have one, even internally.

But if what you want is a way to get from an oid to a function that will convert raw values into Python instances, psycopg2.extensions.string_types is actually already what you need. It might look like it's just a mapping from oid to a name, but that's not quite true: its values aren't strings, they're instances of psycopg2._psycopg.type. Time to delve into a little code.

psycopg2 exposes an API for registering new type converters which we can use to trace back into the C code involved with typecasts; this centers around the typecastObject in typecast.c, which, unsurprisingly, maps to the psycopg2._psycopg.type we find in our old friend string_types. This object contains pointers to two functions, pcast (for Python casting function) and ccast (for C casting function), which would seem like what we want— just pick whichever one exists and call it, problem solved. Except they're not among the attributes exposed (name, which is just a label, and values, which is a list of oids). What the type does expose to Python is __call__, which, it turns out, just chooses between pcast and ccast for us. The documentation for this method is singularly unhelpful, but looking at the C code further shows that it takes two arguments: a string containing the raw value, and a cursor object.

>>> import psycopg2.extensions
>>> cur = something_that_gets_a_cursor_object()
>>> psycopg2.extensions.string_types[23]
<psycopg2._psycopg.type 'INTEGER' at 0xDEADBEEF>
>>> psycopg2.extensions.string_types23
100
>>> psycopg2.extensions.string_types23
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '10.0'
>>> string_types[1114]
<psycopg2._psycopg.type 'DATETIME' at 0xDEADBEEF>
>>> string_types1114
datetime.datetime(2018, 11, 15, 21, 35, 21)

The need for a cursor is unfortunate, and in fact, a cursor isn't always required:

>>> string_types23
100

But anything having to with converting actual string (varchar, for example) types is dependent on the PostgreSQL server's locale, at least in a Python 3 compilation, and passing None to those casters doesn't just fail— it segfaults. The method you mention, cursor.cast(oid, raw), is essentially a wrapper around the casters in psycopg2.extensions.string_types and may be more convenient in some instances.

The only workaround for needing a cursor and connection that I can think of would be to build essentially a mock connection object. If it exposed all of the relevant environment information without connecting to an actual database, it could be attached to a cursor object and used with string_typesoid or with cur.cast(oid, raw), but the mock would have be built in C and is left as an exercise to the reader.

2 of 2
2

The mapping of postgres types and python types is given here. Does that help?

Edit: When you read a record from a table, the postgres (or any database) driver will automatically map the record column types to Python types.

cur = con.cursor()
cur.execute("SELECT * FROM Writers")

row = cur.fetchone()

for index, val in enumerate(row):
    print "column {0} value {1} of type {2}".format(index, val, type(val))

Now, you just have to map Python types to MySQL types while writing your MySQL interface code. But, frankly, this is a roundabout way of mapping types from PostgreSQL types to MySQL types. I would just refer one of the numerous type mappings between these two databases like this

🌐
GeeksforGeeks
geeksforgeeks.org › get-column-names-from-postgresql-table-using-psycopg2
Get column names from PostgreSQL table using Psycopg2 | GeeksforGeeks
September 14, 2021 - This article is an illustration of how to extract column names from PostgreSQL table using psycopg2 and Python.
🌐
GitHub
github.com › psycopg › psycopg2 › issues › 947
Allow users to select columns with complex types · Issue #947 · psycopg/psycopg2
July 24, 2019 - I have a SQL-compliant database that I'm querying with psycopg2. A lot of the columns I need are arrays (mostly arrays of floats, and some more nested maps) However, when getting the results, it seems that everything is converted to strings, and it is unclear how I could get the native types in ...
Author   psycopg
🌐
Safe Community
community.safe.com › home › forums › fme form › authoring › generate list of column names from database table
Generate list of column names from database table | Community
October 18, 2017 - The attached workspace (get-cols.fmw) have a Python Scripted parameter returning a string being a comma separated list of the column names. ... import psycopg2 colnames = [] conn = psycopg2.connect(host="%s"%FME_MacroValues['HOST'], port=FME_MacroValues['PORT'], dbname="%s"%FME_MacroValues['DATABASE'], user="%s"%FME_MacroValues['USER'], password="%s"%FME_MacroValues['PASSWORD']) cur=conn.cursor() cur.execute("select column_name, data_type from INFORMATION_SCHEMA.COLUMNS where table_name = '%s';"%FME_MacroValues['TABLE_NAME']) row = cur.fetchone() while row != None: colnames.append(row[0]) row = cur.fetchone() return ",".join(colnames)
🌐
CommandPrompt Inc.
commandprompt.com › education › how-to-check-column-types-in-postgresql
How to Check Column Types in PostgreSQL — CommandPrompt Inc.
November 28, 2022 - ... In PostgreSQL, the SELECT statement, information_schema, \d command, and pg_typeof() function are used to check the data type of a column. To check/find the data type of a particular column, use the information_schema or pg_typeof() function.
Address   2950 Newmarket ST STE 101 - 231, 98226, Bellingham
🌐
PostgreSQL
postgresql.org › docs › current › datatype.html
PostgreSQL: Documentation: 18: Chapter 8. Data Types
May 14, 2026 - Table 8.1 shows all the built-in general-purpose data types. Most of the alternative names listed in the “Aliases” column are the names used internally by PostgreSQL for historical reasons.
🌐
Dalibo
blog.dalibo.com › 2022 › 06 › 01 › psycopg-row-factories.html
How psycopg row factories help you write safer applications?
June 1, 2022 - def weather_row_factory(cursor): # Extract result set column names. columns = [column.name for column in cursor.description] def make_row(values): # Map column names to values row = dict(zip(columns, values)) return weather_from_row(**row) return make_row · Next we’d arguably get a better design if these functions were class methods of our Weather class above; let’s do it: