I'd like to do a simple check if a variable for boto3 client is empty. I tried following approach:
"""Example of boto3 client lazy initialization"""
from typing import Optional
import boto3
from botocore.client import BaseClient
from mypy_boto3_ec2 import EC2Client
class ClassA:
"""Abstract class which lazily initializes the boto3 client"""
def __init__(self) -> None:
self._client: Optional[BaseClient] = None
self.client_type: str = ""
@property
def client(self):
"""Lazy boto3 client initialization"""
if not self._client:
self._client = boto3.client(self.client_type)
return self._client
class ClassB(ClassA):
"""One of many concrete child classes of the ClassA"""
def __init__(self) -> None:
super().__init__()
self._client: Optional[EC2Client] = None
self.client_type: str = "ec2"
def some_method(self) -> dict:
"""Just a method to try random EC2Client functionality"""
result = self.client.describe_instances()
return result
But this code has many typing problems (a summary of issues found by mypy and pyright):
BaseClient
by concrete class EC2Client
Optional
for the client type, None
value is not compatible with types BaseClient
and EC2Client
describe_instances
.So how do I type an empty variable for a boto3 client? What should be the value so that I recognize the variable is empty? (How do I make it optional without the NoneType attribute problem?)
Questions I used as a sources:
Your class should be generic in the client type - that's the LSP-friendly way to say "attribute is some subtype of X, and classes may want to know which one".
boto3.client
is overloaded in stubs to accept literal service names, but there's no alias to all such names available, so str
with ignore comment is probably your best bet.
Here's what I can suggest keeping your original approach:
from __future__ import annotations
from typing import Generic, TypeVar
import boto3
from botocore.client import BaseClient
from mypy_boto3_ec2 import EC2Client
_C = TypeVar("_C", bound=BaseClient)
class ClassA(Generic[_C]):
"""Abstract class which lazily initializes the boto3 client"""
def __init__(self) -> None:
self._client: _C | None = None
self.client_type: str = ""
@property
def client(self) -> _C:
"""Lazy boto3 client initialization"""
if not self._client:
self._client = boto3.client(self.client_type) # type: ignore[call-overload]
assert self._client is not None
return self._client
class ClassB(ClassA[EC2Client]):
"""One of many concrete child classes of the ClassA"""
def __init__(self) -> None:
super().__init__()
self.client_type: str = "ec2"
def some_method(self) -> dict:
"""Just a method to try random EC2Client functionality"""
result = self.client.describe_instances()
return result
Now the only errors remaining are the true ones:
$ mypy a.py --strict
a.py:33: error: Missing type parameters for generic type "dict" [type-arg]
a.py:36: error: Incompatible return value type (got "DescribeInstancesResultTypeDef", expected "dict[Any, Any]") [return-value]
Found 2 errors in 1 file (checked 1 source file)
But I'd probably simplify this to a cached_property
and restrict the service names to literals:
from __future__ import annotations
from functools import cached_property
from typing import Generic, Literal, TypeAlias, TypeVar
import boto3
from botocore.client import BaseClient
from mypy_boto3_ec2 import EC2Client
_C = TypeVar("_C", bound=BaseClient)
ServiceName: TypeAlias = Literal["ec2", "s3"] # List all clients you're interested in
# To get all service names as of now,
# grep service_name .venv/lib/python*/site-packages/boto3-stubs/__init__.pyi | sed -E 's/.*Literal\["(.+)"\].*/\1/'| sort -u
class ClassA(Generic[_C]):
"""Abstract class which lazily initializes the boto3 client"""
def __init__(self, client_type: ServiceName) -> None:
self.client_type = client_type
@cached_property
def client(self) -> _C:
"""Lazy boto3 client initialization"""
return boto3.client(self.client_type) # type: ignore[return-value]
class ClassB(ClassA[EC2Client]):
"""One of many concrete child classes of the ClassA"""
def __init__(self) -> None:
super().__init__("ec2")
def some_method(self) -> dict:
"""Just a method to try random EC2Client functionality"""
result = self.client.describe_instances()
return result
This still suffers from not checking that service name and service class are compatible. If that's a problem for you, I'd recommend making client
abstract in parent and just implementing it in every child class, removing client_type
dependency altogether. It's just one line instantiating a service class - there's nothing wrong with repeating such code IMO.