Skip to content

FIX: Use covariant Sequence for executemany seq_of_parameters (#572)#586

Open
bewithgaurav wants to merge 2 commits into
microsoft:mainfrom
bewithgaurav:bewithgaurav/fix-572-executemany-typing
Open

FIX: Use covariant Sequence for executemany seq_of_parameters (#572)#586
bewithgaurav wants to merge 2 commits into
microsoft:mainfrom
bewithgaurav:bewithgaurav/fix-572-executemany-typing

Conversation

@bewithgaurav
Copy link
Copy Markdown
Collaborator

Work Item / Issue Reference

GitHub Issue: Closes #572


Summary

Fixes the typing regression reported in #572.

In v1.5.0, the second parameter of Cursor.executemany was typed as:

seq_of_parameters: List[Sequence[Any]]

In v1.6.0 it was widened to also accept pyformat / dict-style parameters:

seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]]

That widening is functionally correct, but it surfaced a long-standing typing problem. typing.List is invariant in its type parameter, so a perfectly valid list[tuple[str, str, str, str, int, str]] argument no longer matches either arm of the Union, and mypy rejects previously valid call sites:

error: Argument 2 to "executemany" of "Cursor" has incompatible type
  "list[tuple[str, str, str, str, int, str]]";
  expected "list[Sequence[Any]] | list[Mapping[str, Any]]"  [arg-type]
note: "list" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance
note: Consider using "Sequence" instead, which is covariant

In v1.5.0 the single-arm List[Sequence[Any]] slipped through mypy's gradual-typing escape hatch on Any. The Union in v1.6.0 forces mypy to commit to one arm, invariance kicks in, and call sites that worked for years break.

The fix

Switch the outer container to the covariant Sequence, on both the public Cursor.executemany and the internal _transpose_rowwise_to_columnwise helper:

seq_of_parameters: Union[
    Sequence[Sequence[Any]],
    Sequence[Mapping[str, Any]],
]

This:

  • Accepts list[tuple[...]], list[list[...]], tuple-of-tuples, generator-backed sequences after list(), exactly what the docstring already promises ("Sequence of sequences or mappings of parameters").etc.
  • Matches PEP 249 (DB-API 2.0), which says executemany takes a sequence of sequences, not a list of sequences.
  • Aligns with how pyodbc types the same parameter.
  • Keeps the v1.6.0 dict / pyformat support intact.
  • Is a purely typing-level runtime behaviour is unchanged.change

Verification

Reproducer derived from the issue:

from mssql_python.cursor import Cursor

def f(curs: Cursor, sql: str) -> None:
    rows: list[tuple[str, str, str, str, int, str]] = [("a", "b", "c", "d", 1, "e")]
    curs.executemany(sql, rows)

…oft#572)

The 1.6.0 signature

    seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]]

regressed type-checking for callers passing a more precise
list type such as list[tuple[str, str, str, str, int, str]].
Because typing.List is invariant, mypy rejects it with:

    error: Argument 2 to "executemany" of "Cursor" has
    incompatible type "list[tuple[...]]"; expected
    "list[Sequence[Any]] | list[Mapping[str, Any]]"  [arg-type]
    note: "list" is invariant -- see ...
    note: Consider using "Sequence" instead, which is covariant

In 1.5.0 the parameter was a single arm (List[Sequence[Any]])
which mypy accepted via the gradual-typing escape hatch on Any;
the union introduced in 1.6.0 forces mypy to pick a matching arm
and invariance kicks in, breaking previously valid call sites.

Switch to the covariant Sequence outer container on both the
public Cursor.executemany and the internal
_transpose_rowwise_to_columnwise helper:

    seq_of_parameters: Union[
        Sequence[Sequence[Any]],
        Sequence[Mapping[str, Any]],
    ]

This:
- accepts list[tuple[...]], list[list[...]], tuple of tuples, etc.;
- matches PEP 249 ("sequence of sequences");
- aligns with pyodbc's typing;
- keeps the 1.6.0 dict/pyformat support intact.

Runtime behaviour is unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 15, 2026 09:24
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses the Cursor.executemany typing regression by changing the outer parameter container from invariant List to covariant Sequence.

Changes:

  • Updates Cursor.executemany parameter typing to accept Sequence[Sequence[Any]] and Sequence[Mapping[str, Any]].
  • Updates the internal row-to-column transposition helper signature similarly.
Comments suppressed due to low confidence (2)

mssql_python/cursor.py:2139

  • The repository still contains mssql_python/mssql_python.pyi with the old Union[List[Sequence[Any]], List[Mapping[str, Any]]] signature for Cursor.executemany. Leaving that stub stale means consumers or tooling that read the stub can continue seeing the invariant List[...] type and the reported typing regression is not fully fixed.
        seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]],

mssql_python/cursor.py:2139

  • This typing regression fix is not covered by an automated type-checking test: the current CI mypy step checks only mssql_python/, and the tests do not include a reproducer that calls executemany with a list[tuple[...]]. Adding a small type-check fixture or equivalent regression check would prevent this public annotation from reverting unnoticed.
        seq_of_parameters: Union[Sequence[Sequence[Any]], Sequence[Mapping[str, Any]]],

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mssql_python/cursor.py
Comment thread mssql_python/cursor.py Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 15, 2026

📊 Code Coverage Report

🔥 Diff Coverage

100%


🎯 Overall Coverage

25%


📈 Total Lines Covered: 7018 out of 27134
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (100%)

Summary

  • Total: 1 line
  • Missing: 0 lines
  • Coverage: 100%

📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.build._deps.simdutf-src.src.haswell.implementation.cpp: 0.4%
mssql_python.pybind.build._deps.simdutf-src.src.implementation.cpp: 6.7%
mssql_python.pybind.build._deps.simdutf-src.include.simdutf.implementation.h: 10.4%
mssql_python.pybind.build._deps.simdutf-src.include.simdutf.scalar.utf16_to_utf8.utf16_to_utf8.h: 25.3%
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 59.7%
mssql_python.pybind.build._deps.simdutf-src.include.simdutf.internal.isadetection.h: 65.3%
mssql_python.row.py: 70.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 74.2%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@bewithgaurav bewithgaurav force-pushed the bewithgaurav/fix-572-executemany-typing branch from c7d496c to ba2e287 Compare May 15, 2026 09:42
Two follow-ups from the Copilot PR review on microsoft#586:

1. mssql_python/mssql_python.pyi still carried the old invariant
   signature

       seq_of_parameters: Union[List[Sequence[Any]], List[Mapping[str, Any]]]

   Sync it to the covariant Sequence form to match cursor.py and
   prevent a silent regression if the stub is ever relocated to
   __init__.pyi (where mypy would actually consume it).

2. _transpose_rowwise_to_columnwise is only ever called with
   already-converted positional rows (pyformat-to-qmark conversion
   happens upstream in executemany before transposition).
   Advertising it as accepting Mapping rows is misleading - the
   transpose loop would yield mapping keys, not values. Narrow its
   annotation to Sequence[Sequence[Any]] only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bewithgaurav bewithgaurav force-pushed the bewithgaurav/fix-572-executemany-typing branch from ba2e287 to d41013f Compare May 15, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invariant Type on Cursor.executemany

2 participants