mrkeyoor.com_
Wed 23 Sept 00:35 UTC
PyPIDataupdated 22 Sept 2026

oracledb review

python-oracledb 4.0.2 is Oracle's DB API 2.0 driver and the supported successor to cx_Oracle. Default Thin mode connects to Oracle Database 12.1 or newer without Oracle Client libraries. Thick mode loads Oracle Client 19 or later when an application needs older database interoperability or a feature unavailable in Thin mode. Beyond cursors and transactions, the driver covers pools, PL/SQL, array DML, Advanced Queuing, notifications, data-frame interchange, and Thin-only async connections. Our wheel included compiled extensions and typing metadata.

Verdict

python-oracledb 4.0.2 installed in 0.5 seconds, occupied 24 MB, and imported in 0.48 seconds in our sandbox with 0 audit findings. It is the right direct Oracle driver when Thin or Thick mode matches the database; it is not an ORM and does not make wallets, native clients, pools, or transaction boundaries disappear.

We installed it

Lab card: what happened when we installed oracledbScreenshot of oracledb documentation
Install✓ · 0.5s5 packages on disk · 24 MB
Importimport oracledb in 0.48s · compiled extensions · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does oracledb install cleanly?

Yes. In a fresh container with an empty cache, pip install oracledb finished in 0.5s, leaving 5 packages and 24 MB on disk. pip-audit reported no known vulnerabilities.

What does oracledb need to run?

Python >=3.9, and a platform wheel with compiled extensions. In our run import oracledb succeeded in 0.48s, and the package ships py.typed for type checkers.

oracledb or SQLAlchemy: which should you use?

SQLAlchemy: You want an ORM or composable SQL layer and accept that an Oracle DB API driver is still required underneath. python-oracledb 4.0.2 installed in 0.5 seconds, occupied 24 MB, and imported in 0.48 seconds in our sandbox with 0 audit findings.

When should you not use oracledb?

You can choose another database and use none of Oracle's specific behavior. This driver does not remove Oracle licensing, administration, or SQL dialect work.

API stability4/5Connections, cursors, bind variables, transactions, and exception classes follow Python DB API 2.0, and the project documents migration from the obsolete cx_Oracle driver. Version 4 retains that basic model while adding Oracle-specific capability around it. Thin and Thick modes remain a meaningful compatibility boundary: they support different database versions and some different features. Code that depends on an implicit mode, compatibility alias, or session default needs explicit tests before a major upgrade.
Docs5/5The official documentation provides installation matrices, a Thin versus Thick feature table, DSN examples, pool tuning, SQL and PL/SQL calls, bind rules, asyncio, wallets, security, troubleshooting, high availability, and an API reference. The repository's samples include synchronous and asynchronous variants. The hard part is choosing the correct Oracle concept: service name, TNS alias, wallet, DRCP, and failover settings solve different problems, so readers still need database-specific knowledge.
Maintenance5/5Oracle publishes and maintains the driver. PyPI dates version 4.0.2 to July 14, 2026, while GitHub shows an unarchived repository pushed on August 20 with 24 open issues and pull requests. PyPI offers many platform wheels plus a source distribution, which matters because our installed artifact included compiled extensions. The repository couples code with tests, samples, security reporting, and compatibility documentation for current Oracle Database and Client releases.
Ecosystem5/5The weekly figure supplied for this guide is 6,342,485 downloads, and GitHub reports 449 stars. python-oracledb is the documented replacement for cx_Oracle and is used underneath SQLAlchemy's Oracle dialect as well as through direct DB API calls. Its feature surface includes queues, notifications, data-frame exchange, PL/SQL, pools, and Oracle high-availability options. That breadth is useful only inside the Oracle ecosystem; it does not create portability to another SQL database.

Use it if

  • Python code needs Oracle's maintained DB API driver for direct SQL, binds, cursors, and transactions.
  • The target is Oracle Database 12.1 or newer and deployment should avoid a separate Oracle Client install by using Thin mode.
  • The application uses PL/SQL types, array DML, Advanced Queuing, session pools, notifications, or Oracle high-availability features.
  • Async connections and pools are required and the project can stay in Thin mode.
Skip it if

Setup reality

We installed oracledb 4.0.2 in 0.5 seconds in a fresh Python 3.12 Bookworm sandbox. It left 5 packages and 24 MB on disk, and pip-audit reported 0 known vulnerabilities. The package declares 15 direct dependencies, requires Python 3.9 or newer, ships compiled .so extensions plus py.typed, and completed import oracledb in 0.48 seconds.

Thin mode is the default and needs no Oracle Client, but it requires Oracle Database 12.1 or later. Supply credentials outside source and use an Easy Connect string such as host:port/service_name, or configure the documented TNS and wallet path for the deployment. Service names, SIDs, RAC descriptors, proxy users, and Autonomous Database wallets are different connection cases; a generic database URL cannot stand in for all of them.

Call oracledb.init_oracle_client() before creating any connection or pool to enter Thick mode. Oracle Client 19 or later must be discoverable by the operating system, and Python plus client libraries need matching architectures. Put the native client and its system libraries into the real container image, then run a startup connection there. A developer machine with a globally installed client can hide a broken deployment.

Autocommit is off by default. Closing a connection rolls back unfinished work, and pooled sessions must not return with accidental transaction state. Bind values rather than formatting SQL, but remember that binds cannot replace table or column names. Size each process's pool against the database session budget. Async code uses connect_async() or create_pool_async() and works only in Thin mode; cursor creation itself remains local and synchronous.

Patterns

Connect in default Thin mode connect-thin

import os
import oracledb

with oracledb.connect(
    user=os.environ['DB_USER'],
    password=os.environ['DB_PASSWORD'],
    dsn='db.example.com:1521/appsvc',
) as connection:
    print(connection.version)

Thin mode needs no Oracle Client and connects to Oracle Database 12.1 or newer.

Enable Thick mode before connecting enable-thick-mode

import oracledb

oracledb.init_oracle_client(lib_dir='/opt/oracle/instantclient_23_8')

connection = oracledb.connect(
    user='app',
    password='secret',
    dsn='db.example.com/appsvc',
)

`init_oracle_client()` must run before the first pool or connection. Python and Oracle Client architectures must match.

Execute a query with named bind variables query-with-binds

sql = '''
    select employee_id, first_name, salary
    from employees
    where department_id = :department_id
    order by employee_id
'''

with connection.cursor() as cursor:
    cursor.execute(sql, department_id=50)
    for employee_id, name, salary in cursor:
        print(employee_id, name, salary)

Bind the values to avoid SQL injection and repeated parsing. Identifiers such as table names cannot be bind variables.

Return query rows as dictionaries fetch-dictionaries

def dict_row(cursor):
    columns = [item[0].lower() for item in cursor.description]
    cursor.rowfactory = lambda *values: dict(zip(columns, values))

with connection.cursor() as cursor:
    cursor.execute('select employee_id, first_name from employees')
    dict_row(cursor)
    rows = cursor.fetchall()

`cursor.description` exists after execution, so assign the row factory only after the query has run.

Insert a row and commit explicitly insert-and-commit

with connection.cursor() as cursor:
    cursor.execute(
        'insert into audit_log (event_id, message) values (:1, :2)',
        [event_id, message],
    )
connection.commit()

Autocommit is disabled by default; closing this connection before `commit()` would roll the insert back.

Insert many rows with executemany bulk-insert

rows = [(101, 'queued'), (102, 'queued'), (103, 'queued')]

with connection.cursor() as cursor:
    cursor.setinputsizes(int, 20)
    cursor.executemany(
        'insert into jobs (job_id, status) values (:1, :2)',
        rows,
    )
connection.commit()

`setinputsizes()` avoids repeated allocation in large batches. Enable batch errors only when partial success is intentional.

Create and use a fixed-size connection pool connection-pool

pool = oracledb.create_pool(
    user=os.environ['DB_USER'],
    password=os.environ['DB_PASSWORD'],
    dsn=os.environ['DB_DSN'],
    min=4,
    max=4,
    increment=0,
)

with pool.acquire() as connection:
    with connection.cursor() as cursor:
        value, = cursor.execute('select 1 from dual').fetchone()

Multiply this pool size by the worker-process count and compare the result with the database session limit.

Run a query with an async connection async-query

import oracledb

async def load_employee(employee_id):
    connection = await oracledb.connect_async(
        user='app', password='secret', dsn='db.example.com/appsvc'
    )
    async with connection:
        with connection.cursor() as cursor:
            await cursor.execute(
                'select first_name from employees where employee_id = :1',
                [employee_id],
            )
            return await cursor.fetchone()

Async connections work only in Thin mode. Creating the cursor is local, so that line is deliberately not awaited.

Acquire from an async connection pool async-pool

pool = oracledb.create_pool_async(
    user='app', password='secret', dsn='db.example.com/appsvc',
    min=2, max=10, increment=1,
)

async with pool.acquire() as connection:
    with connection.cursor() as cursor:
        await cursor.execute('select systimestamp from dual')
        row = await cursor.fetchone()

`create_pool_async()` returns the pool immediately; connection work occurs while the pool establishes or acquires sessions.

Call a PL/SQL procedure with an output value call-plsql-procedure

with connection.cursor() as cursor:
    total = cursor.var(oracledb.NUMBER)
    cursor.callproc('billing.calculate_total', [invoice_id, total])
    print(total.getvalue())

Use `cursor.var()` for an OUT parameter, then read its value after the PL/SQL call returns.

Read a CLOB value read-clob

with connection.cursor() as cursor:
    cursor.execute('select document_body from documents where document_id = :1', [doc_id])
    lob, = cursor.fetchone()
    text = lob.read() if lob is not None else None

LOB handles depend on the active connection. Read or stream the value before releasing a pooled session.

Inspect an Oracle database error handle-database-errors

try:
    cursor.execute(sql, values)
except oracledb.DatabaseError as exc:
    error, = exc.args
    print(error.full_code, error.message)
    if error.code == 1:
        raise ValueError('duplicate value') from exc
    raise

Match the documented numeric Oracle code. Message text varies with database version and language.

Alternatives

PackageRegistryPick it when
SQLAlchemyPyPIYou want an ORM or composable SQL layer and accept that an Oracle DB API driver is still required underneath
pyodbcPyPIYour organization standardizes on ODBC across databases and can install and configure an Oracle ODBC driver
JayDeBeApiPyPIYou must use an existing Oracle JDBC setup from Python and can accept a JVM bridge in the process

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.