Skip to content

Type "def func1() -> Unknown | def func2() -> Unknown" is not callable #406

Open

Description

@jschear

I'm using a feature flag to migrate from usage of one macro to another in a Bazel codebase, using roughly this structure:

def func1():
    pass

def func2():
    pass

my_func = func1 if True else func2

my_func()

starpls emits a warning on my_func():

Type "def func1() -> Unknown | def func2() -> Unknown" is not callable

Ideally the type checker could see that both branches of the optional type are themselves callable, and avoid a warning here.

This is using starpls v0.1.21, along with --experimental_infer_ctx_attributes, --experimental_use_code_flow_analysis, and --experimental_enable_label_completions.

Activity

  1. jschear commented on Dec 17, 2025

    @jschear
    Author

    I'd like to try to contribute a fix here, especially if I can get a pointer to relevant code. I'm guessing this falls under "code flow analysis"?

  2. Ahajha commented on Oct 17, 2026

    @Ahajha

    @jschear If you still want to help out, take a look at #443 for some inspiration. I think there's 2 paths we can take:

    1. We strip the func1 and func2 pieces and do something similar to my PR, where we special case the unioning of two functions with the same signature. So def func1() -> Unknown | def func2() -> Unknown would become def () -> Unknown (a nameless function type). It may make sense to stack the change on top of my branch.
    2. Special case the union type so that it is callable iff all of its members are callable.

    I could see some arguments for 2, since we don't lose information, but 1 produces a simpler type and the fix should be simpler.

    FYI, the author is quite busy with $dayjob and hasn't been able to look at the repo in a while. I've been maintaining a fork here https://github.com/modular/starpls, happy to cherry pick fixes or if you want to submit a PR there I'm happy to merge it. (I'll be cherry picking my own PR there in the next few days.)

  3. jschear commented on Oct 23, 2026

    @jschear
    Author

    Hi @Ahajha, thanks for the info. I'm no longer at the $dayjob that motivated this issue, so no longer in any real rush to fix it.

    My more complete example looked something like this, where the functions didn't have the exact same type signature, so option 1 wouldn't have fixed my particular issue.

    # in defs.bzl
    def func1(use_foo_feature = False):
        pass
    
    def func2(**kwargs):
        pass
    
    my_func = func1 if True else func2
    
    # in BUILD
    my_func(
      name = "MyTarget",
      use_foo_feature = True,
    )

    But in general it makes sense to me that the union type A | A should be treated as A, so even if that wouldn't have fixed my issue, it might still be a good change? I'll dig in a bit.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions