Skip to content

Misbehavior of f.__code__.co_firstlineno for decorated functions #139838

Open

Description

@picnixz

Bug report

Bug description:

See also: #139783

Compare:

dummy = lambda f: f                 # L1
@dummy                              # L2
def f():                            # L3
    return                          # L4
print(f.__code__.co_firstlineno)    # L5  # result = 2

with

dummy = lambda f: f                 # L1
pass                                # L2
def f():                            # L3
    return                          # L4
print(f.__code__.co_firstlineno)    # L5  # result = 3

and

dummy = lambda f: f                 # L1
@\
dummy                               # L3
def f():                            # L4
    return                          # L5
print(f.__code__.co_firstlineno)    # L6  # result = 3

In the last example, the line continuation after @ is not handled correctly and the first line for f.__code__ then becomes the line at dummy. Because of that, the values returned by inspect.findsource(f) will not be consistent and inspect.getsourcelines will not use the correct source (it will only start at "dummy" and will not contain the "@" part).

@serhiy-storchaka suggested that this is an issue in the AST parser. I however don't know how we should actually fix this.

Consequences to inspect.getsource and other introspection functions:

import inspect

def outer(*args, **kwargs):
    def inner(f):
        return f
    return deco

@(
    outer(
        ...
    )
)
def f():
    return "hello"

print(inspect.getsource(f))

On Python 3.12, the output is expected:

@(
    outer(
        ...
    )
)
def func():
    return "hello"

On Python 3.13.7, the output is missing the first line @(:

    outer(
        ...
    )
)
def func():
    return "hello"

On Python 3.13.9 and main, we are missing @ and outer parentheses:

    outer(
        ...
    )

CPython versions tested on:

CPython main branch

Operating systems tested on:

No response

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    on Oct 9, 2025
  2. serhiy-storchaka commented on Oct 9, 2025

    @serhiy-storchaka
    Member

    This is a complex issue. The range of FunctionDef starts with the def line, it does not include decorators. The decorator_list attribute is a list of decorator expressions. Expression does not include @, so its range does not include the line with a single @\. co_firstlineno is calculated as minimum of lineno of the function and all decorator expressions.

    Decorated functions and classes are the only nodes whose range do not include ranges of all its subnodes. We can change this by making their range including decorators (with @), this is relatively easy. This can only be done in a new release. We will lost information about the line containing def or class. I do not know what will be an impact of this.

    Other option -- change AST even more. Instead of the decorator_list attribute of function and class nodes containing the list of decorators, introduce a Decorated node that wraps the function or class node and contains the list of decorators. Then we will have the position of def or class as well as the position of the first @, and the ranges will children will be always included in the range of the parent. But this will require changes in the code generator and some other code.

    There may be old issues for this problem.

  3. picnixz commented on Oct 9, 2025

    @picnixz
    MemberAuthor

    Expression does not include @, so its range does not include the line with a single @. co_firstlineno is calculated as minimum of lineno of the function and all decorator expressions.

    Yes that's what I observed.

    We will lost information about the line containing def or class

    I think it will have an impact for analysis tools. Especially, static ones that could only rely on reading the file. An alternative to keep the current behavior could be to have an additional attribute that includes the entire range. That way we know the effective location of def and its surrounding.

  4. picnixz commented on Oct 9, 2025

    @picnixz
    MemberAuthor

    Actually, it may not necessarily be that bad. In codegen_function:

        firstlineno = s->lineno;
        if (asdl_seq_LEN(decos)) {
            firstlineno = ((expr_ty)asdl_seq_GET(decos, 0))->lineno;
        }

    So, we're already considering firstlineno to be the lineno of the first decorator. The decorators are arbitrary nodes but we could add where the decorator really started. And here, firstlineno wouldn't be lineno but atsymbol_lineno for instance.

  5. picnixz commented on Oct 9, 2025

    @picnixz
    MemberAuthor

    I am trying to modify the parser but I never touched the grammar file and thus I'm not sure how I can make @ as part of the line being parsed. AFAIK, this is the only case where we have a non-keyword word which produces an AST node.

  6. serhiy-storchaka commented on Oct 9, 2025

    @serhiy-storchaka
    Member

    See also #77392 and #9731. So the first idea would return the old behavior which had its own issues.

  7. picnixz commented on Oct 10, 2025

    @picnixz
    MemberAuthor

    Arf, I see. I don't have much time today but I'll try to have a look tomorrow.

  8. dmadisetti commented on Oct 17, 2025

    @dmadisetti
  9. changed the title [-]Misbehavior of `f.__code__.co_firstlineno` for `@\`-decorated functions[/-] [+]Misbehavior of `f.__code__.co_firstlineno` for decorated functions[/+] on Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-parsertype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions