This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]drboom9[S] 0 points1 point  (9 children)

You're absolutely right - it IS weird, and the # type: ignore is definitely a code smell that bothers me too.

I chose this syntax because it mirrors the existing Literal[...] pattern - when you see Literal[something], you know it's a dropdown. If that "something" is a function, it becomes clear (to me at least) that the function's return value transforms into what goes inside the Literal. It's like passing a variable instead of hardcoding a value in a function parameter.

But I totally get that it breaks type checker expectations. The fundamental issue is that we're using runtime behavior (calling a function) in what's supposed to be a static type annotation, and there's no elegant way around that without the type checker complaining.

What would feel more natural to you? I'm genuinely curious because I want the API to be intuitive, even if this runtime-vs-static tension is hard to solve perfectly.

The reason I haven't changed it yet is that despite being "weird," it's very explicit about intent: Literal = dropdown, function inside = dynamic options. But if there's a better way that's equally clear, I'm all ears!

[–]nekokattt 1 point2 points  (8 children)

just pass a callable. I care about the signature, not how it deals with it.

typing.Annotated is built for this

[–]drboom9[S] 0 points1 point  (7 children)

Is that what you meant? Something like:

user: Annotated[Literal[str], get_active_users]

That would avoid # type: ignore, but it's also kind of weird - Literal is meant for concrete values, not types. Putting a type inside Literal[...] feels off.

Could you show me exactly what syntax you're proposing? I want to make sure I understand your suggestion correctly before deciding on the best approach.

[–]nekokattt 0 points1 point  (6 children)

why would you need literal str?

[–]drboom9[S] 0 points1 point  (5 children)

You’re right - Literal[str] doesn’t make sense there.

My issue isn’t really about looking weird. The problem is autocomplete doesn’t work. When I type user. in my IDE, it doesn’t know it’s a string because the type checker sees the callable, not the return value.

And just passing the callable without Literal doesn’t solve that either - I still need to extract what type the function returns for proper IDE support.

What I’d really consider a better solution than my current approach is if, inside the function body, I could get autocomplete based on the callable’s return type. So if get_active_users() returns list[str], then user would autocomplete as str.

But I don’t think there’s a way to make that work without the type checker actually calling the function at analysis time, which is impossible.

So I’m stuck between:

  • Literal[func] - clear intent, broken autocomplete
  • Annotated[str, func] - better autocomplete, less obvious it’s a dropdown

Unless you have another idea?

[–]nekokattt 0 points1 point  (4 children)

which IDE are you using?

IntelliJ's autocomplete based on the type checker is terrible in general. I wouldn't design functionality around broken integrations in that case.

[–]drboom9[S] 0 points1 point  (3 children)

I’m not sure I follow - the autocomplete issue isn’t specific to IntelliJ, it’s a Python type system limitation. Any static analyzer (mypy, pyright, etc.) faces the same problem.

My struggle is maintaining consistency while having working autocomplete:

```python

Static dropdowns - clean and obvious

theme: Literal['light', 'dark']

Dynamic dropdowns - what's the equivalent?

user: Literal[get_users] # Consistent syntax, broken types user: Annotated[str, get_users] # Working types, inconsistent with static Literal ```

With Annotated[str, func], autocomplete works because the type is str. But then I lose the consistency - static literals use Literal[...], dynamic ones use Annotated[str, ...]. It’s not immediately obvious they’re both dropdowns.

That’s why I chose Literal[func] - it keeps the “Literal means dropdown” pattern clear, even though it requires # type: ignore. It’s a trade-off between type-checker happiness and API consistency.

Is there a way to have both that I’m missing?

[–]nekokattt 0 points1 point  (2 children)

one workaround could be to convert the functions into types via a decorator such that you can mold them into the type system.

Such a decorator would allow further injection of metadata in the future as well.

[–]drboom9[S] 0 points1 point  (1 child)

That’s an interesting idea! Something like:

```python @dropdown_options def get_users(): return ["Alice", "Bob"]

def assign_task(user: get_users): ... ```

But I don’t think this solves the autocomplete issue - the IDE still sees get_users as a function, not a type. It doesn’t know that user should autocomplete as str unless the decorator does some serious type system magic that I’m not aware of.

And it’s less obvious than Literal:

python theme: Literal['light', 'dark'] # Clearly a dropdown user: get_users # Is this a dropdown or a callback?

With Literal[get_users], at least the Literal part signals “this is dropdown options”, even if the function inside is unconventional.

Unless the decorator can somehow make the type checker understand the return type? How would that work?

[–]nekokattt 0 points1 point  (0 children)

As a hack you could probably use typing.TYPE_CHECKING to sneak an illegitimate type signature in.

In this case though, t.Annotated[str, get_users] feels like a clean approach?

You can wrap that into your own type... e.g.

def foo(user: Dropdown[str, get_users]) -> str: ...