Fix Python Autocomplete for Custom Generic Dictionary Classes
In a Python developer community I participate in, a member recently asked: “I’ve built a custom class that wraps a dictionary to add some extra logic, but whenever I access an item using obj['key'], my IDE (VS Code/PyCharm) has no idea what the type is. How do I get the autocomplete hints to work for items in my generic dictionary class?”
This is a common frustration when building data structures or API wrappers. By default, if you don’t explicitly tell Python’s type system what your class contains, it treats everything as Any, effectively killing your IDE’s ability to help you.
The Original Question
Section titled “The Original Question”The developer had code that looked roughly like this:
class Storage: def __init__(self): self._data = {}
def __getitem__(self, key): return self._data[key]
def __setitem__(self, key, value): self._data[key] = value
# Usagestore = Storage()store["user"] = User(name="Alice")user = store["user"]# user. <--- No autocomplete for 'name' here!The Immediate Fix
Section titled “The Immediate Fix”To fix this, you must use Generics. By using typing.TypeVar and typing.Generic, you inform the static type checker that the class is a container for specific types defined at instantiation.
Solution (Python 3.9+):
from typing import TypeVar, Generic, Dict
K = TypeVar("K")V = TypeVar("V")
class Storage(Generic[K, V]): def __init__(self) -> None: # Note: Use dict[K, V] in 3.9+, or Dict[K, V] for older versions self._data: dict[K, V] = {}
def __getitem__(self, key: K) -> V: return self._data[key]
def __setitem__(self, key: K, value: V) -> None: self._data[key] = value
# Now autocomplete works!store: Storage[str, User] = Storage()store["user"] = User(name="Alice")user = store["user"]# user.name -> Autocomplete now suggests 'name'Detailed Explanation
Section titled “Detailed Explanation”When you create a custom class, tools like Pylance (VS Code) or the PyCharm inspector perform “static analysis.” They don’t run your code; they read the signatures. Without hints, __getitem__ is assumed to return Any.
1. Why TypeVar and Generic are necessary
Section titled “1. Why TypeVar and Generic are necessary”TypeVar creates a placeholder (a “type variable”). When you define class Storage(Generic[K, V]), you are telling Python: “This class handles two types, K and V, and they will be decided when the user creates an instance.”
2. Inheriting from Collection Abstractions
Section titled “2. Inheriting from Collection Abstractions”While the “Immediate Fix” above works for basic access, it doesn’t make your class “behave” like a dictionary in the eyes of the type system (e.g., it won’t have .get(), .keys(), or .items() unless you write them).
For a robust solution, you should inherit from collections.abc.MutableMapping.
Solution for Python 3.11+ (Industry Standard):
from collections.abc import MutableMappingfrom typing import TypeVar, Iterator
K = TypeVar("K")V = TypeVar("V")
class AdvancedStorage(MutableMapping[K, V]): def __init__(self) -> None: self._data: dict[K, V] = {}
def __getitem__(self, key: K) -> V: return self._data[key]
def __setitem__(self, key: K, value: V) -> None: self._data[key] = value
def __delitem__(self, key: K) -> None: del self._data[key]
def __iter__(self) -> Iterator[K]: return iter(self._data)
def __len__(self) -> int: return len(self._data)
# This version provides hints for .get(), .update(), etc., automatically.Edge Cases and Considerations
Section titled “Edge Cases and Considerations”- Fixed Key Types: If your dictionary will always use strings as keys, you don’t need
K. You can simplify it toclass Storage(Generic[V])and hardcodekey: strin your methods. - The
UserDictAlternative: If you don’t want to implement all the abstract methods ofMutableMapping, you can inherit fromcollections.UserDict. However, you still need to applyGenericto ensure the type hints pass through to the underlying data. - Performance: Type hints are purely for the IDE and type checkers (like Mypy). They have zero impact on runtime performance.
Related Concerns
Section titled “Related Concerns”Does this work with older Python versions?
Section titled “Does this work with older Python versions?”If you are on Python 3.8 or lower, you cannot use dict[K, V] or list[T]. You must import the capitalized versions from the typing module:
# Python 3.8 and belowfrom typing import Dict, TypeVar, GenericK = TypeVar("K")V = TypeVar("V")self._data: Dict[K, V] = {}What if I want to restrict what types can be stored?
Section titled “What if I want to restrict what types can be stored?”You can constrain your TypeVar. For example, if your storage should only ever hold subclasses of a BaseModel, you can define it like this:
V = TypeVar("V", bound=BaseModel)class ModelStorage(Generic[V]): ...This prevents a developer from accidentally putting a string or an integer into a storage container designed for complex objects.
Why isn’t my IDE picking up the types even with Generic?
Section titled “Why isn’t my IDE picking up the types even with Generic?”Check your IDE settings. In VS Code, ensure “Type Checking Mode” is set to basic or strict in the Pylance settings. In PyCharm, ensure your interpreter is correctly pointed to the virtual environment where your code resides. Sometimes, a simple “Restart Extension Host” or IDE restart is required to refresh the type cache after changing generic signatures.