ruff/crates/ty_python_semantic/resources/mdtest/call/getattr_static.md
Brent Westbrook 88c0ce3e38
Some checks are pending
CI / cargo build (release) (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Update default and latest Python versions for 3.14 (#20725)
Summary
--

Closes #19467 and also removes the warning about using Python 3.14
without
preview enabled.

I also bumped `PythonVersion::default` to 3.9 because it reaches EOL
this month,
but we could also defer that for now if we wanted.

The first three commits are related to the `latest` bump to 3.14; the
fourth commit
bumps the default to 3.10.

Note that this PR also bumps the default Python version for ty to 3.10
because
there was a test asserting that it stays in sync with
`ast::PythonVersion`.

Test Plan
--

Existing tests

I spot-checked the ecosystem report, and I believe these are all
expected. Inbits doesn't specify a target Python version, so I guess
we're applying the default. UP007, UP035, and UP045 all use the new
default value to emit new diagnostics.
2025-10-07 12:23:11 -04:00

4.2 KiB

inspect.getattr_static

Basic usage

inspect.getattr_static is a function that returns attributes of an object without invoking the descriptor protocol (for caveats, see the official documentation).

Consider the following example:

import inspect

class Descriptor:
    def __get__(self, instance, owner) -> str:
        return "a"

class C:
    normal: int = 1
    descriptor: Descriptor = Descriptor()

If we access attributes on an instance of C as usual, the descriptor protocol is invoked, and we get a type of str for the descriptor attribute:

c = C()

reveal_type(c.normal)  # revealed: int
reveal_type(c.descriptor)  # revealed: str

However, if we use inspect.getattr_static, we can see the underlying Descriptor type:

reveal_type(inspect.getattr_static(c, "normal"))  # revealed: int
reveal_type(inspect.getattr_static(c, "descriptor"))  # revealed: Descriptor

For non-existent attributes, a default value can be provided:

reveal_type(inspect.getattr_static(C, "normal", "default-arg"))  # revealed: int
reveal_type(inspect.getattr_static(C, "non_existent", "default-arg"))  # revealed: Literal["default-arg"]

When a non-existent attribute is accessed without a default value, the runtime raises an AttributeError. We could emit a diagnostic for this case, but that is currently not supported:

# TODO: we could emit a diagnostic here
reveal_type(inspect.getattr_static(C, "non_existent"))  # revealed: Never

We can access attributes on objects of all kinds:

import sys

reveal_type(inspect.getattr_static(sys, "dont_write_bytecode"))  # revealed: bool
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = EllipsisType) -> Any
reveal_type(inspect.getattr_static(inspect, "getattr_static"))

reveal_type(inspect.getattr_static(1, "real"))  # revealed: property

(Implicit) instance attributes can also be accessed through inspect.getattr_static:

class D:
    def __init__(self) -> None:
        self.instance_attr: int = 1

reveal_type(inspect.getattr_static(D(), "instance_attr"))  # revealed: int

And attributes on metaclasses can be accessed when probing the class:

class Meta(type):
    attr: int = 1

class E(metaclass=Meta): ...

reveal_type(inspect.getattr_static(E, "attr"))  # revealed: int

Metaclass attributes can not be added when probing an instance of the class:

reveal_type(inspect.getattr_static(E(), "attr", "non_existent"))  # revealed: Literal["non_existent"]

Error cases

We can only infer precise types if the attribute is a literal string. In all other cases, we fall back to Any:

import inspect

class C:
    x: int = 1

def _(attr_name: str):
    reveal_type(inspect.getattr_static(C(), attr_name))  # revealed: Any
    reveal_type(inspect.getattr_static(C(), attr_name, 1))  # revealed: Any

But we still detect errors in the number or type of arguments:

# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`"
inspect.getattr_static()

# error: [missing-argument] "No argument provided for required parameter `attr`"
inspect.getattr_static(C())

# error: [invalid-argument-type] "Argument to function `getattr_static` is incorrect: Expected `str`, found `Literal[1]`"
inspect.getattr_static(C(), 1)

# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
inspect.getattr_static(C(), "x", "default-arg", "one too many")

Possibly unbound attributes

import inspect

def _(flag: bool):
    class C:
        if flag:
            x: int = 1

    reveal_type(inspect.getattr_static(C, "x", "default"))  # revealed: int | Literal["default"]

Gradual types

import inspect
from typing import Any

def _(a: Any, tuple_of_any: tuple[Any]):
    reveal_type(inspect.getattr_static(a, "x", "default"))  # revealed: Any | Literal["default"]

    # revealed: def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
    reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))