Skip to content

[BUG] 3.3.0 regression: dict[str, str].get(missing_key) is typed as str, so the returned None takes str fast paths (bool/len/in give wrong results) #7991

Closed

Description

@ChCh27

Since 3.3.0, the declared item type of a dict[str, str] is used to type the result of .get(key) as str. When the key is missing, .get() returns None, but the generated code applies the str fast paths to it without a None check. Truthiness silently returns the wrong answer, and len() reads out of bounds.

Reproducer

# g.py
def truth(d: dict[str, str]):
    v = d.get('missing')
    return bool(v)

  def length(d: dict[str, str]):
      v = d.get('missing')
      return len(v)

def contains(d: dict[str, str]):
    v = d.get('missing')
    return 'a' in v

Important

The bug affects not only missing keys but also None values:

def length(d: dict[str, str]):
   v = d.get('none')
   return len(v)  
length({'none': None})
# setup.py
from setuptools import setup, Extension
from Cython.Build import cythonize

setup(name='g', ext_modules=cythonize([Extension("g", ["g.py"])],
      compiler_directives={"language_level": 3}))

Called as f({'a': 'b'}), so the key is missing and .get() returns None:

CPython Cython 3.2.5 Cython 3.3.0
bool(v) False False True
len(v) TypeError TypeError 4294967295
'a' in v TypeError TypeError True

v == 'a' and v + 'a' behave correctly.

Generated C

3.3.0, for bool(v) — no None check:

Py_ssize_t __pyx_temp = __Pyx_PyUnicode_IS_TRUE(__pyx_v_v);

3.2.5, same line:

if (__pyx_v_v == Py_None) __pyx_t_1 = 0;
else {
  Py_ssize_t __pyx_temp = __Pyx_PyUnicode_IS_TRUE(__pyx_v_v);
  ...
}

The assignment itself explicitly permits None:

if (!(likely(PyUnicode_CheckExact(__pyx_t_1))||((__pyx_t_1) == Py_None) || __Pyx_RaiseUnexpectedTypeError("str", __pyx_t_1))) ...

so nothing raises there — only the later fast paths assume a real str.

Notes

Environment

Cython 3.3.0 (vs 3.2.5), CPython 3.12.3, Linux x86_64 (also observed on aarch64), gcc 15.

This hit us in production: a value read with .get() from a dict[str, str] | None passed an if v guard while being None, so None flowed into code that could not receive it.

Activity

  1. scoder commented on Sep 13, 2026

    @scoder
    Contributor
  2. matusvalo commented on Sep 13, 2026

    @matusvalo
    Contributor

    i confirm the bug. But I am puzzled what is purpose of the test __pyx_t_1 == Py_None in line:

    if (!(likely(PyUnicode_CheckExact(__pyx_t_1))||((__pyx_t_1) == Py_None) || __Pyx_RaiseUnexpectedTypeError("str", __pyx_t_1))) __PYX_ERR(0, 4, __pyx_L1_error)

    It should not guard against None value? And if it is not guarding what is the purpose of that expression? Update: The condition just allows None and str values... Now it is clear for me the purpouse of the test...

    Also it is interesting that annotating variable instead it adds correct None guard:

    def main(d: dict):
        v: str = d.get('missing')
        return len(v)

    Produces the same code just adds extra None guard:

       if (unlikely(__pyx_v_v == Py_None)) {
         PyErr_SetString(PyExc_TypeError, "object of type \047NoneType\047 has no len()");
         __PYX_ERR(0, 5, __pyx_L1_error)
       }
       __pyx_t_2 = __Pyx_PyUnicode_GET_LENGTH(__pyx_v_v); if (unlikely(__pyx_t_2 == ((Py_ssize_t)-1))) __PYX_ERR(0, 5, __pyx_L1_error)
       __pyx_t_1 = PyLong_FromSsize_t(__pyx_t_2); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 5, __pyx_L1_error)
       __Pyx_GOTREF(__pyx_t_1);

    Hence two points are unclear for me:

    • What is the purpose of the first Py_None equality test Update, now I understand that code it basically allows str or None values ...
    • Why plain str variable annotation adds second (correct) guard but inference to the same type (str) does not.
  3. matusvalo commented on Sep 13, 2026

    @matusvalo
    Contributor

    OK the fix was more simpler than I thought. Fixing PR is linked to this issue.

  4. added this to the 3.3.1 milestone on Sep 13, 2026
  5. matusvalo commented on Sep 15, 2026

    @matusvalo
    Contributor

    I updated description with one additional case.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions