Random Python Things I Learned From Vibe Coding, Part 1
Posted by Al Sweigart in misc
I've been using LLMs to create ButtonPad, a simple Python GUI framework that makes it easy to create apps with a grid of programmable buttons. As part of this, I'd like to take the time to review the code and document anything I don't already understand. This can be somewhat embarassing, I open myself up to ridicule in the form of feigned surprise: "You didn't know that? I thought you were a Python book author." But the chief criticism of AI-assisted coding is that it's so easy to create things that you don't understand, you'll never actually bother to learn it. I set myself for vulnerability (a comment to my previous blog post: "Jesus, you managed to suck ass at vibe coding. Literal dumbass have figured that out.") but I still want like to set an example that nobody is born knowing all of this. Here's what I've learned.
from future import annotations
The LLM put from __future__ import annotations
as the first line of my program, but I don't exactly understand what this does and why I need it.
I make moderate use of type hints so that tools like Mypy can do type checking, and then potential uncover bugs early in the coding process. Type hints themselves are a particular use of function annotations, introduced in Python 3.0 from PEP 3107 way back in 2006. I know that the from __future__
statements are so that earlier versions of Python can make use of syntax from future versions so your code is more backwards compatible with older Python versions. This is important for making Python packages/libraries/frameworks so they can be compatible with a wider range of Python interpreters.
The main from __future__
use I know is from __future__ import division
, where your Python 2 interpreter can use Python 3-style division. In Python 3, the division operator always results in a float, so 5 / 2
results in the float 2.5
. But back in Python 2, division only results in a float if one of the numbers in the operation was a float. So in Python 2 evaluates 5 / 2.0
or 5.0 / 2
or 5.0 / 2.0
as 2.5
. But plain division with two integers like 5 / 2
does integer division: 2
By putting from __future__ import division
at the top of your program (future imports must always be the first thing in your program aside from comments), even Python 2 will run the program and use Python 3-style division.
PEP 563 – Postponed Evaluation of Annotations introduced a change where annotations weren't immediately evaluated. This was necessary because otherwise the code in this anno_example.py file would be impossible:
# anno_example.py
class A:
def func(self) -> A:
return A()
...
You can have a method return an object of type A
if the A
class statement hasn't finished. The change from PEP 563 is good, but it did make some code backwards incompatible because now the type hints and annotations are stored as strings rather than evaluated objects. (This is where I'm getting lost; I can't think of any backwards incompatible code examples.)
Here's the thing: here in September 2025, no version of Python has this "postponed evaluation of annotations" at all. It was supposed to be in 3.10, but got pushed back. Even Python 3.14 can't run the anno_example.py program without generating this error:
Traceback (most recent call last):
File "/Users/al/anno_example.py", line 1, in <module>
class A:
File "/Users/al/anno_example.py", line 2, in A
def func(self) -> A:
^
NameError: name 'A' is not defined
What `from __future__ import annotations` does exactly
But if I include from __future__ import annotations
at the start of the program, this error doesn't happen because the A
in the -> A
type hint is treated like a string, and it doesn't matter that the class A
statement hasn't finished creating the A
class yet.
I did more research and found that if you want this behavior for your Python app (and you almost always do) or if your code is only using annotations for type hints, then you should include from __future__ import annotations
to avoid these NameErrors. I'll have to go back through my other projects and start including this. (Though none of them have ever ended up needing it.)
The only time you wouldn't want to include it is if your code uses the __annotations__
attribute of a function, say func.__annotations__
in our earlier example. But for modern Python code, instead of doing that, use typing.get_type_hints
instead.
Forward References in Type Hints
I encountered one bit of code like this: ElementLike = Union["BPButton", "BPLabel", "BPTextBox"]
I wondered why it defined ElementLike
here when it could have just used _BPBase
, the parent class of BPButton
, BPLabel
, and BPTextBox
. Then I realized all of those classes hadn't been defined yet. However, that's when I noticed that the members of the Union
were strings: "BPButton"
, etc. This makes sense: You can't have ElementLike = Union[BPButton, BPLabel, BPTextBox]
because those classes haven't been defined yet. Hence why strings are used. I found out that these are called forward references in Python.
I thought about replacing all the instances of ElementLike
with the parent class _BPBase
, but then realized the reason the AI didn't do that was because "ElementLike" is a much more readable name than just the parent class's name and it follows Python's idea of duck typing over strict class hierarchies. I decided to give it a better name (BPWidgetType
) and go with that instead of _BPBase
. After all, I didn't want to accept a _BPBase
object itself, only its three children.
I decided to read more about forward references in the Python documentation, which led me down a rabbit hole about typing.TypeAlias
(added in 3.10 with PEP 613), type aliases, the type
statement (added in 3.12) where type
is a soft keyword (added in 3.10).
All of this is in the kept-up-to-date Specification for the Python Type System document.
I'll put a pin in these and come back to them later in future blog posts.
Specifying Function Signature Types
I found this line that the AI had generated to create a type for the callback functions you give the buttons to call when they are clicked. These function calls are passed the widget object that was clicked, along with the x and y coordinate in the ButtonPad grid. They return None
.
`Callback = Optional[Callable[["BPWidgetType", int, int], None]]`
I've used the typing.Callable
type before, but I don't use type hints or functions that take function arguments all that often. It doesn't surprise me that there's a way to be more specific than just Callable
. I assumed (and verified from this Stack Overflow post that Callable[["BPWidgetType", int, int], None]
is for functions ("callables"), the ["BPWidgetType", int, int]
means the function takes three arguments (a BPWidgetType
and two ints), and returns None
. Buttons don't require callbacks, so this function signature is itself wrapped in Optional[]
.
I wanted to create a less generic name though, so I renamed Callback
to BPCallback
.
Dataclasses
I know what dataclasses are, but I keep forgetting exactly what they give. Dataclasses are for when you don't want to store data just as a key-value dictionary, but you don't really need a class because you don't have methods. Dataclasses were proposed in PEP 557 added in 3.7.
Here's the example from the dataclasses documentation:
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
What dataclasses automatically give are:
- The ability to just list fields: a member variable with a type and a default value.
- An
__init__()
that you can set these member variables from. __repr__()
and__eq__()
methods.
You can also enable:
order=True
to the@dataclass
decorator to make__lt__()
,__le__()
,__gt__()
, and__ge__()
methods.unsafe_hash=True
to make the__hash__()
method.frozen=True
to make setting values to the fields raise an exception.
The LLM generated the following code for fonts in ButtonMap:
@dataclass
class _FontSpec:
name: str = "TkDefaultFont"
size: int = 12
I can see why the LLM used a dataclass, but it's really overkill for this use case. I'll switch it to just two properties named font_name
and font_size
.
Multiline "import as"
The LLM generated the following:
from pymsgbox import (
alert as _pymsgbox_alert,
confirm as _pymsgbox_confirm,
prompt as _pymsgbox_prompt,
password as _pymsgbox_password,
)
I knew about the "import as" syntax that lets you rename it. That was needed here beause the original PyMsgBox functions were going to be wrapped. But I guess I never realized that you could split them up across several lines by putting them in parentheses.