Skip to content

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

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.

Metadata

Metadata

Labels

bugSomething 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