Skip to content

Recognizing expressions with a constant value. #12583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mrolle45 opened this issue Apr 14, 2022 · 3 comments
Closed

Recognizing expressions with a constant value. #12583

mrolle45 opened this issue Apr 14, 2022 · 3 comments

Comments

@mrolle45
Copy link

Feature

Determining the actual value of a variable (or an expression) if it can be determined to be constant.

Pitch

mypy tries to determine the type of an expression already, and if it involves typed functions or known operators using values of known type, then it can infer the type of this function or operation. And so on.

There are some cases in which it would be useful for mypy to determine the actual value, not just the type, when the value can be determined statically.

def foo(x: Literal[7]): pass
var: Final = 3 + 4
foo(var)

should pass type checking. Better yet, the inferred type of var could be Literal[7] instead of int.
Even if var is not Final, mypy would recognize that its value is 7, until possibly reassigned later in the same scope.

I think that mypy could evaluate an arbitrary expression safely by compiling it and executing the compiled code, under limited conditions. All names in the expression must have a known value, or be builtin functions. Thingsl ike n += 1 inside a loop can't be evaluated. No user-defined functions or classes. Any value returned from the compiled code (if no exception is raised) would be deemed to be the fixed value.

The known value could include tuples, lists, etc. You might ask why these would be useful, since they cannot be used in a Literal type. However, such a collection might be subscripted, as in ['a', 3][1] having a constant value of 3 which can match a Literal[3] type.
More important is the value of __all__ when it is not an actual list or tuple expression. The user could have some value like "a b".split(), or list1 + list2; or have a statement like __all__.append("c"). Generally, if __all__ has a value at the end of the module code that is some fixed Iterable[str] then mypy should use this to determine the exported names from the module. This would be a solution for #12582, and I won't have to convert my split()s to lists in order to pass type checking.

As a speedup, the analysis of fixed value of a variable could be postponed until the variable is actually used where a Literal type is expected, or if the module is used in import * in a different module. In the latter case, the names mentioned in __all__ are in the globals already, but the public/private status of each global name only matters for purposes of import *.

@erictraut
Copy link

There is some overlap between this proposal and the "literal math" proposal, although the latter doesn't require running arbitrary code during analysis time.

I won't speak for the maintainers of mypy, but I think it seems like executing arbitrary code as part of type analysis would be ill advised — both for safety and performance reasons. Mypy is, after all, a static analyzer. There are other tools that perform runtime type validation, but that's outside the scope of mypy.

@dvarrazzo
Copy link

dvarrazzo commented Oct 8, 2023

I would like to add another use case for this; however, as @erictraut suggests, this might be outside mypy scope.

In psycopg we are moving to auto-generate sync code from async code. We do this with AST transformation. There are snippets of code that we just don't want on the sync side or on the async side, are just not valid on the sync side or on the async side, so we guard them with if True: and if False. For instance this:

if True:  # ASYNC
    import sys
    import asyncio
    from asyncio import Lock
else:
    from threading import Lock
...
        if True:  # ASYNC
            if sys.platform == "win32":
                loop = asyncio.get_running_loop()
                if isinstance(loop, asyncio.ProactorEventLoop):
                    raise e.InterfaceError(...)

Both Python and MyPy are able to prune the unneeded branch, while the transformation script will prune the async-side of the if in order to delete code that might be invalid in the sync side.

This works, however is relatively ugly, and requires the use of a third party project such as ast-comments in order to retain the comment in the ast (we would probably use this project anyway to retain the comment in the generated sync output).

I tried to replace the pattern using if "async" and if not "async" but, although it works as expected in Python, it doesn't work in mypy, which doesn't recognise one of the branches as dead.

For instance, this code shows an error (in mypy 1.5.1), but no error if the test is replaced by if True.

async def f() -> None:
    pass

async def g() -> None:
    if "async":
        await f()
    else:
        f()

It would be great if the else-side branch of if "async" (and the if-side branch of if not "async") were recognised as dead code by MyPy.

@brianschubert
Copy link
Collaborator

Mypy performs constant folding as of v1.0.0 (#14283). The code in the original post now passes type checking.

We can track more advanced cases of constant evaluation in their own issues. For example, #11616.

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

No branches or pull requests

6 participants