# Expose special method lookup at Python level

**URL:** <https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236>\
**Category:** Ideas\
**Created:** [February 21, 2026, 10:16am UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236 "2026-02-21T10:16:54Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![tanloong](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/tanloong/32/22285_2.png) [@tanloong](https://discuss.python.org/u/tanloong)\
**Post date:** [February 21, 2026, 10:16am UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/1 "2026-02-21T10:16:54Z")

</div>

## Background

There’s a recurring need in the standard library to look up special methods (like ` __enter__ `, ` __exit__ `, etc.). Currently, libraries like `contextlib` implement their own logic using [`getattr_static`](https://github.com/python/cpython/blob/main/Lib/inspect.py#L1750).

A recent issue ([#144386](https://github.com/python/cpython/issues/144386)) discovered a behavioral difference between the `with` statement and `contextlib.ExitStack.enter_context()` when context manager methods are defined via ` __slots__ `. The fix ([#144420](https://github.com/python/cpython/pull/144420)) added descriptor handling to `contextlib`, but [as noted](https://github.com/python/cpython/pull/144420#discussion_r2759368458) in the review, using `getattr_static` is “heavy machinery” and we need a C-level function exposed at the Python level.

This would also resolve the [#62912](https://github.com/python/cpython/issues/62912) where `operator.index()`'s pure Python lookup doesn’t match the C implementation due to the lack of a proper special method lookup API.

## Proposal

Expose `_PyObject_LookupSpecialMethod` (the function used by the `LOAD_SPECIAL` bytecode) as a Python function.

## Open Questions for Discussion

### 1. Function naming

What should the function be called? (Current PR ([#144990](https://github.com/python/cpython/pull/144990)) uses `lookup_special_method`).

### 2. Module placement

Which module should the function be placed in? (Current PR puts it in `types` module).

### 3. Return value semantics

The `_PyObject_LookupSpecialMethod` has an optimization: for method descriptors (regular functions), it returns the **unbound function** rather than a bound method to avoid creating temporary objects. This means the caller must pass the instance as the first argument.

Should the public API:

- **Option A** : Follow the C function exactly (return unbound for functions, call ` __get__ ` for other descriptors)
- **Option B** : Always return a bound method (simpler for users, but less efficient and inconsistent with `LOAD_SPECIAL`)

(Current PR chooses Option A).

### 4. Error handling

- Return `None` if the special method is not found?
- Raise `AttributeError`?

(Current PR returns `None` if the special method is not found).

## Related Issues

- [#62912](https://github.com/python/cpython/issues/62912): Issue about `operator.index` doesn’t match the C version
- [#144989](https://github.com/python/cpython/issues/144989) Issue about exposing `_PyObject_LookupSpecialMethod`
- [#144990](https://github.com/python/cpython/pull/144990): PR to expose `_PyObject_LookupSpecialMethod`

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [February 21, 2026, 10:52am UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/2 "2026-02-21T10:52:30Z")

</div>

> [@tanloong](#):
>
> - **Option A** : Follow the C function exactly (return unbound for functions, call ` __get__ ` for other descriptors)
> - **Option B** : Always return a bound method (simpler for users, but less efficient and inconsistent with `LOAD_SPECIAL`)

Does Option A require the user on the python side to do some further case distinction? If so, how does that look?

---

<div class="post-metadata">

**Author:** ![storchaka](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/storchaka/32/217_2.png) [@storchaka](https://discuss.python.org/u/storchaka)\
**Post date:** [February 21, 2026, 11:59am UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/3 "2026-02-21T11:59:36Z")

</div>

In most cases we need not `_PyObject_LookupSpecialMethod`, which is an optimization for C and requires the caller to handle the case when `self` should be passed explicitly during the call, but `_PyObject_LookupSpecial`, which just returns a callable.  
Yes, it adds some overhead, but handling the special case to avoid that overhead in Python adds more overhead (it is different in C).

So this part should be changed. We may need a function that looks up the name without using the descriptor protocol, but this is a different case.

The function should not return None if the special method is not found, otherwise how can we distinguish this from None assigned to the special name? The C code does not interpret None as “not found”. I think the function should have interface similar to `getattr()`.

What module? I don’t like putting more stuff which is not builting types in the `types` module. The `operator` module is other candidate. It already contain the `_compare_digest()` function which can only be implemented in C. We should accept that the experiment of implementing `operator` in Python failed.

---

<div class="post-metadata">

**Author:** ![MegaIng](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/megaing/32/16162_2.png) [@MegaIng](https://discuss.python.org/u/MegaIng)\
**Post date:** [February 21, 2026, 12:51pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/4 "2026-02-21T12:51:27Z")

</div>

> [@storchaka](#):
>
> The `operator` module is other candidate. It already contain the `_compare_digest()` function which can only be implemented in C. We should accept that the experiment of implementing `operator` in Python failed.

It does not contain `_compare_digest`. Only `_operator` does and this function is not imported inside of `operator`.

`_operator` is “misused” to provide a function for `hmac.py`. This isn’t an argument for exposing this function in `operator.py`.

* * *

Independent of that I think `operator` is a good candidate. The other alternative is `inspect`, but that’s expensive to import (although maybe lazy imports fix this?).

---

<div class="post-metadata">

**Author:** ![picnixz](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/picnixz/32/12660_2.png) [@picnixz](https://discuss.python.org/u/picnixz)\
**Post date:** [February 21, 2026, 3:34pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/5 "2026-02-21T15:34:01Z")

</div>

> `_operator` is “misused” to provide a function for `hmac.py`. This isn’t an argument for exposing this function in `operator.py`.

More precisely, it’s there because we need an unconditional module that is always shipped with the interpreter (we can’t put it in `_hashlib` because the latter is conditioned to OpenSSL, and other modules may have been too costly to import at that time or even less relevant). HMAC did not have a built-in module either so we couldn’t put it there.

* * *

If we need a module where we expose some C functions directly, did we consider a new dedicated module for that? Or an alternative builtin (though it may be too niche…). Maybe a `getattr_static` builtin could be reasonable? Or offer a ctypes-like safe interface for exposing interpreter-level functions without creating a module just to hold them?

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [February 21, 2026, 10:46pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/6 "2026-02-21T22:46:23Z")

</div>

> [@tanloong](#):
>
> ### 4. Error handling
> 
> - Return `None` if the special method is not found?
> - Raise `AttributeError`?
> 
> (Current PR returns `None` if the special method is not found).

Is returning `None` here really a smart play? If I am performing a lookup, it could very well result in some attribute being `None`.

I.e. imagine this:

```python
class C1:
    attr = None

lookup(C1, "attr") # Returns `None` (valid)
lookup(C1, "typo") # Returns `None`, so I don't notice the typo unless I change the value of attr

```

I think it’s pretty simple to see where I’m going with this, but lMO raising an error is the most logical thing here to do (similar to `getattr` and `getattr_static`).

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [February 21, 2026, 10:50pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/7 "2026-02-21T22:50:03Z")

</div>

Imo a `_capi` module could work well here, although it would be hard to argue in favor of one for non C implementations. In that case, maybe a `_pyapi` module would be more fitting.

---

<div class="post-metadata">

**Author:** ![ncoghlan](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ncoghlan/32/14266_2.png) [@ncoghlan](https://discuss.python.org/u/ncoghlan)\
**Post date:** [February 22, 2026, 1:25am UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/8 "2026-02-22T01:25:52Z")

</div>

The workaround for private implementation APIs is off-topic, since we’re talking about a new public API.

I’m personally OK with using the `types` module, as that has long been about exposing the type system _machinery_ in addition to exposing specific implementation types. The main alternative I can see would be a static method on `type`, but I don’t see any compelling advantage to that over a module level function.

For the technical details:

- I agree with Serhiy that the subtle C optimisation of special method lookup should _not_ be exposed, since the convenience/speed trade-off is worth it in C, but would be far more dubious in a public Python API where special casing returned functions involves a much more expensive type check than the optimised check that C consumers can perform.
- I also agree we should raise an exception for failed lookups

---

<div class="post-metadata">

**Author:** ![tanloong](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/tanloong/32/22285_2.png) [@tanloong](https://discuss.python.org/u/tanloong)\
**Post date:** [February 22, 2026, 4:26pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/9 "2026-02-22T16:26:41Z")

</div>

Quick update: modified the [PR](https://github.com/python/cpython/pull/144990) based on the consensus reached by now:

1. Switched to `_PyObject_LookupSpecial` instead of `_PyObject_LookupSpecialMethod` and accordingly renamed (for now) the function from `lookup_special_method` to `lookup_special`.
2. Raise `AttributeError` if the special method is not found.
3. Add optional `default` parameter similar to `getattr`.

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [February 23, 2026, 12:14pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/10 "2026-02-23T12:14:02Z")

</div>

I’ve looked at the implementation in `types.py` a bit, and IMO caling the last argument `args` is missleading. Apart from that, using `*args` allowes for users to do `lookup_special(obj, "doesnt_exist", 1, 2, 3)` which is a bit wired considering that we should only allow one single `default` argument. Perhaps we could change the signature of `lookup_special` to `def lookup_special(object, name, default=__sentinel): ...` and filtering out `__sentinel` lateron. Sure in the [C code](https://github.com/tanloong/cpython/blob/expose-lookupspeicalmethod/Modules/_typesmodule.c#L20) its called `*args`, similar to `builtin_getattr`’s definition in [`bltinmodule.c`](https://github.com/python/cpython/blob/main/Python/bltinmodule.c#L1303), but IMO, calling it `default` (like in the [stubs](https://github.com/python/typeshed/blob/main/stdlib/builtins.pyi#L1533-1542)) would make more sense.

I could quickly make a PR to your branch, so you can see what i mean in more detail.

---

<div class="post-metadata">

**Author:** ![tstefan](https://avatars.discourse-cdn.com/v4/letter/t/f475e1/32.png) [@tstefan](https://discuss.python.org/u/tstefan)\
**Post date:** [February 23, 2026, 6:17pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/11 "2026-02-23T18:17:28Z")

</div>

> [@JoBe](#):
>
> Sure in the [C code](https://github.com/tanloong/cpython/blob/expose-lookupspeicalmethod/Modules/_typesmodule.c#L20) its called `*args`

but that does not only refer to the optional `default` parameter, but to _all_ parameters.

Another small quirk: the current python implementation is

```python
    def lookup_special(object, name, default=_sentinel):

```

with a `, /` missing. Both C code and docs specify that the parameters are positional only. Following the spirit of PEP 399, the signatures and actually also the docstrings of corresponding C and Python functions should agree.

---

<div class="post-metadata">

**Author:** ![JoBe](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/jobe/32/26961_2.png) [@JoBe](https://discuss.python.org/u/JoBe)\
**Post date:** [February 23, 2026, 6:19pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/12 "2026-02-23T18:19:36Z")

</div>

Oh, seems like I’ve missed that (I had only in the Stubs I was preparing iirc).

As for the part about `*args` I guess my reasoning is weak, but it should still be obvious why calling it `default` rather than `*args` makes more sense.

---

<div class="post-metadata">

**Author:** ![bluetech](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/bluetech/32/8859_2.png) [@bluetech](https://discuss.python.org/u/bluetech)\
**Post date:** [April 10, 2026, 10:41pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/13 "2026-04-10T22:41:21Z")

</div>

Recording a possible use case for `lookup_special`.

In pytest, we currently discover fixture definitions on an object by iterating `dir(obj)`. Problem is, `dir` sorts but we want to preserve definition order. We could sort for `firstlineno`, but that seems slow. So I looked at how `dir` is implemented, and as far as I can see, it would be possible to implement an order-preserving `dir` using `lookup_special` as `lookup_special(obj, " __dir__")()`. I’m considering using a “polyfill” like this in the meantime, though it’s probably inaccurate and slow in itself, so probably not…

```python
def lookup_special(obj: object, name: str):
    cls = type(obj)
    for base in cls. __mro__ :
        if name in base. __dict__ :
            descriptor = base. __dict__ [name]
            descriptor_type = type(descriptor)
            if hasattr(descriptor_type, " __get__"):
                return descriptor_type. __get__ (descriptor, obj, cls)
            return descriptor
    return None

```

Of course this is a bit of an XY problem, as ideally there would be a `dir(obj, sort=False)` option, but that seems unlikely.

---

<div class="post-metadata">

**Author:** ![Anonymous941](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/anonymous941/32/35621_2.png) [@Anonymous941](https://discuss.python.org/u/Anonymous941)\
**Post date:** [May 24, 2026, 4:52pm UTC](https://discuss.python.org/t/expose-special-method-lookup-at-python-level/106236/14 "2026-05-24T16:52:28Z")

</div>

That is indeed inaccurate in a number of edge cases (although probably good enough for your purposes). Here’s an example:

```python
class ExampleMeta(type):
    __mro__ = None

class Example(metaclass=ExampleMeta):
    def __repr__ (self):
        return "example"

example = Example()

repr(example) # example
lookup_special(example, " __repr__") # TypeError: 'NoneType' object is not iterable

```

Also, returning `None` is not the same as `NULL`, and is ambiguous because is a valid return value for `_PyObject_LookupSpecialMethod`.

Here’s a version that I believe handles all edge cases (except for [not ready types](https://docs.python.org/3/c-api/type.html#c.PyType_Ready), but they shouldn’t be exposed to the Python interpreter). It uses built-in `method_wrapper` objects on `type` to bypass magic methods. I license it as CC0.

```python
def lookup_dict(typ: type, attribute: str, /) -> Any:
    "Searches a type's __dict__ and all superclasses' __dict__ for attribute."
    dictionary_method_wrapper: MethodWrapperType = cast(Mapping, type. __dict__ )[
        " __dict__"
    ]. __get__
    mro_method_wrapper: MethodWrapperType = cast(Mapping, type. __dict__ )[
        " __mro__"
    ]. __get__

    mro = mro_method_wrapper(typ)
    for base in mro:
        dictionary = dictionary_method_wrapper(base)
        try:
            return dictionary[attribute]
        # a TypeError can happen if a non-hashable object is passed into the attribute parameter, and this would cause a NULL pointer to be returned
        # to mirror behavior exactly, catch TypeError too
        except (TypeError, KeyError):
            pass
    raise AttributeError(f"attribute {repr(attribute)} not found")

def lookup_special(obj: object, method: str, /) -> Any:
    try:
        descriptor_or_method = lookup_dict(type(obj), method)
    except AttributeError:
        raise AttributeError(f"special method {repr(method)} not found")
    else:
        # call __get__ if it's a descriptor
        descriptor_type = type(descriptor_or_method)
        try:
            get = lookup_dict(descriptor_type, " __get__")
        except AttributeError:
            return descriptor_or_method
        else:
            return get(descriptor_or_method, obj, type(obj))

```
