Как отмечают другие авторы, в динамически типизированном языке это не так просто. Вы не можете просто отследить присвоение обратно до объявления статического типа, как вы можете это сделать в C или Java.
Однако, часто можно сделать разумное определение типа.
Предположительно, правила области видимости позволяют определить, к какому i (или к какому набору я) можно обращаться / обновлять / связывать, где задается вопрос («какой тип в этой точке в коде?»). Затем можно выполнить анализ всех значений, которые могут быть назначены (особенно тривиальный случай, когда я связан только с определением функции). Верхняя граница в решетке типов для этих типов является «типом» i. Да, это может быть «что угодно» в некоторых случаях, но в большинстве хорошо написанных программ даже динамические переменные имеют «узкий» тип, предназначенный программистом, и часто это примитивный тип языка (например, «int»). Или же программист не сможет написать разумный алгоритм (Что, ваш индекс массива иногда не является целым числом?).
Вам нужно провести какой-то консервативный анализ программы, чтобы определить этот тип верхней границы. (Очевидно, что вы можете сделать тривиальный анализ и сделать бесполезный вывод, что переменная может быть «любого» типа). Я думаю, что это неудовлетворительный ответ.
Механизм для выполнения всего этого анализа довольно сложен (вам нужен глобальный анализ потоков и некоторое определение того, что можно динамически загрузить, чтобы сделать это действительно хорошо), и я сомневаюсь, что пакет Python AST делает это.