Skip to content

return in a finally block at cursor.py:3046 breaks import on Python 3.14 and discards fetch errors #712

Closed

Description

@rgant

Describe the bug

mssql_python/cursor.py:3046 puts a bare return inside a finally block. The block belongs to the batch_generator generator in Cursor.arrow_reader. The construct arrived in #644 and shipped in 1.13.0.

This has two effects.

1. Python 3.14 refuses to import the driver under warnings-as-errors.

PEP 765 makes return in a finally block a SyntaxWarning in Python 3.14. A project that turns warnings into errors cannot import mssql_python at all:

Exception message:
  File "/path/to/site-packages/mssql_python/cursor.py", line 3046
    return
    ^^^^^^
SyntaxError: 'return' in a 'finally' block

Stack trace:
  mssql_python/__init__.py:54:  from .db_connection import connect, Connection
  mssql_python/db_connection.py:9:  from mssql_python.connection import Connection, TokenProvider
  mssql_python/connection.py:22:  from mssql_python.cursor import Cursor

Version 1.12.0 does not carry this line. Version 1.13.0 does. I compiled cursor.py from both wheels to confirm that.

2. The return discards an exception raised inside the generator.

The guard reads:

cur = cursor_ref[0]
cursor_ref[0] = None
if cur is None or cur.closed or cur.hstmt is None:
    return

If arrow_batch() raises and the guard is true, the return replaces the exception with a normal end of iteration. The caller reads a truncated result set and sees no error. I did not confirm that a closed cursor and an in-flight exception occur together in practice. The language semantics hold either way.

The comment above the block states that the teardown runs on "an exception inside the body". The return is the one path where that intent does not hold.

To reproduce

Effect 1:

import warnings

warnings.simplefilter('error')
import mssql_python  # SyntaxError: 'return' in a 'finally' block

Effect 2, reduced to the same shape as batch_generator:

def make(guard):
    def gen():
        try:
            yield 1
            raise RuntimeError('fetch failed')
        finally:
            if guard:
                return          # same shape as cursor.py:3046
            print('cleanup ran')
    return gen

g = make(True)()
next(g)
try:
    next(g)
except RuntimeError:
    print('propagated')
except StopIteration:
    print('the RuntimeError was discarded')   # this branch runs

g = make(False)()
next(g)
try:
    next(g)
except RuntimeError:
    print('cleanup ran, then propagated')     # this branch runs

Expected behavior

The driver imports under warnings-as-errors. An exception from arrow_batch() reaches the caller.

A return is not needed here. If the guard becomes a positive condition around the cleanup body, both effects go away:

cur = cursor_ref[0]
cursor_ref[0] = None
if cur is not None and not cur.closed and cur.hstmt is not None:
    ...  # existing cleanup

Further technical details

Python version: 3.14.5
mssql-python version: 1.13.0
SQL Server version: Azure SQL Database
Operating system: macOS 26.6.1

Additional context

The import failure blocks every test that imports the driver, because our test suite treats warnings as errors. A per-message warning filter works as a local workaround.

Activity

  1. github-actions commented on Aug 10, 2026

    @github-actions

    Hi J Rob Gant (@rgant), thank you for opening this issue!

    Our team will review it shortly. We aim to triage all new issues within 24-48 hours and get back to you.

    If you have additional information to share, please feel free to update the issue.

    Thank you for your patience!

  2. added
    bugSomething isn't working
    triage doneIssues that are triaged by dev team and are in investigation.
    and removed
    triage neededFor new issues, not triaged yet.
    on Aug 12, 2026
  3. subrata-ms commented on Aug 12, 2026

    @subrata-ms
    Contributor

    Root Cause Analysis

    The regression was introduced in mssql-python 1.13.0 by PR #644, which added explicit cleanup behavior to Cursor.arrow_reader().

    As part of that change, batch_generator gained a try/finally block. Its defensive cleanup guard used a bare return when the cursor or statement handle was unavailable.

    A return inside finally has two consequences:

    Python 3.14 emits a PEP 765 SyntaxWarning. Applications treating warnings as errors cannot compile or import the module from source.
    If arrow_batch() raises while the guard is true, the return overrides the active exception. Iteration ends with StopIteration, potentially presenting a truncated result set as successful.
    Version 1.12.0 was unaffected because its Arrow batch generator had no cleanup finally block. The issue is limited to the control-flow statement used by the cleanup guard, rather than the Arrow fetching or ODBC cleanup operations themselves.

  4. subrata-ms commented on Aug 18, 2026

    @subrata-ms
    Contributor

    The issue is addressed and fix should be available as part of upcoming mssql-python driver release 1.14.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

FIXEDbugSomething isn't workingtriage doneIssues that are triaged by dev team and are in investigation.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions