I don’t view this as an issue. Errors can be raised anywhere, we don’t (and never should) have checked exceptions, this isn’t a type safety issue.
The place where 1 is significantly better:
row: tuple[str, str] = sql_conn.execute(
"SELECT name, phone from users WHERE user_id = ?",
(user_id,),
).fetch_one()
If fetch_one
returns tuple[Any, ...]
By being a gradual type, the annotation here just works for something the type system can’t know about.
Yes, this leaves more correctness up to the user, but it also doesn’t create extra work and boilerplate in places the type system can’t handle.
Similarly,
result: tuple[int, int, int, int] = struct.unpack("!4B", buffer)
There are plenty of cases in real world code where not claiming to know more than we actually do and deffering to the programmer’s annotations is preferable.