36
loading...
This website collects cookies to deliver better user experience
>>> type(lambda x: x)
<class 'function'>
>>> type(type)
<class 'type'>
>>> type(23)
<class 'int'>
>>> type(("127.0.0.1", 8000))
<class 'tuple'>
("127.0.0.1", 8000)
the function returns type as tuple.>>>from django.contrib.auth.models import User
>>>type(User.objects.filter(
email='[email protected]'))
django.db.models.query.QuerySet
django.db.models.query.QuerySet
. addr = "127.0.0.1"
port = 8000
reveal_type((addr, port))
type
function, the static type checker provides reveal_type
function returns the type of the argument during static type checker time. The function is not present during Python runtime but is part of mypy.$mypy filename.py
note: Revealed type is
'Tuple[builtins.str, builtins.int]'
Tuple[builtins.str, builtins.int]
. The reveal_type function also returns the type of tuple elements. In contrast, the type function returns the object type at the first level.# filename.py
from django.contrib.auth.models import User
reveal_type(User.objects.filter(
email='[email protected]'))
$ mypy filename.py
note: Revealed type is
'django.contrib.auth.models.UserManager
[django.contrib.auth.models.User]'
reveal_type
returns the type as UserManager[User]
. Mypy is interested in the type of objects at all levels.# mypy.ini
exclude = "[a-zA-Z_]+.migrations.|[a-zA-Z_]+.tests.|[a-zA-Z_]+.testing."
allow_redefinition = false
plugins =
mypy_django_plugin.main,
[mypy.plugins.django-stubs]
django_settings_module = "yourapp.settings"
django_settings_module
variable in mypy.plugins.django-stubs
.from datetime import date
# Example variable annotation
lang: str = "Python"
year: date = date(1989, 2, 1)
# Example annotation on input arguments
# and return values
def sum(a: int, b: int) -> int:
return a + b
class Person:
# Class/instance method annotation
def __init__(self, name: str, age: int,
is_alive: bool):
self.name = name
self.age = age
self.is_alive = is_alive
lang: str = "Python"
. The grammar is name: <type> = <value>
.sum(a: int, b: int) -> int
. The function sum
input arguments annotation looks similar to variable annotation. The return value annotation syntax, ->
arrow mark followed by return value type.
In sum function definition, it's -> int
.self
or class
argument needs no annotation since mypy understand the semantics of the declaration. Except __init__
method, when the function, method does return value, the explicit annotation should be -> None
.class-based views
and function-based views
. Since function and method annotations are similar, the example will focus on function-based views.from django.http import (HttpRequest, HttpResponse,
HttpResponseNotFound)
def index(request: HttpRequest) -> HttpResponse:
return HttpResponse("hello world!")
HttpRequest
and returns a HttpResponse
. The annotating view function is straightforward after importing relevant classes from django.http
module.def view_404(request:
HttpRequest) -> HttpResponseNotFound:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
def view_404(request: HttpRequest) -> HttpResponse:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
# bad - not precise and not useful
def view_404(request: HttpRequest) -> object:
return HttpResponseNotFound(
'<h1>Page not found</h1>')
view_404
. The function returns HttpResponseFound
- Http Status code 404. The return value annotation can take three possible values - HttpResponseNotFound, HttpResponse, object
. The mypy accepts all three annotations as valid. >>>HttpResponse.mro()
[django.http.response.HttpResponse,
django.http.response.HttpResponseBase,
object]
>>>HttpResponseNotFound.mro()
[django.http.response.HttpResponseNotFound,
django.http.response.HttpResponse,
django.http.response.HttpResponseBase,
object]
HTTPResponseNotFound
inherits HttpResponse
, HTTPResponse
inherits HttpResponseBase
, HttpResponseBase
inherits objects
. HTTPResponseNotFound
is a special class of HTTPResponse
and object
; hence mypy doesn't complain about the type mismatch.from django.db import models
from django.utils import timezone
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
def create_question(question_text: str) -> Question:
qs = Question(question_text=question_text,
pub_date=timezone.now())
qs.save()
return qs
Question
is a Django model with two explicit fields: question_text of CharField
and pub_date of DateTimeField
. create_question
is a simple function that takes in question_text
as an argument and returns Question
instance. def get_question(question_text: str) -> Question:
return Question.objects.filter(
question_text=question_text).first()
get_question
takes a string as an argument and filters the Question model, and returns the first instance.error: Incompatible return value type
(got "Optional[Any]", expected "Question")
from typing import Optional
def get_question(question_text: str) -> Optional[Question]:
return Question.objects.filter(
question_text=question_text).first()
Optional
type, which means None. The return value Optional[Question] means None type or Question type.
# mypy.ini
strict_optional = False
def get_question(question_text: str) -> Question:
return Question.objects.filter(
question_text=question_text).first()
strict mode
. strict_optional
variable instructs mypy to ignore None type in the annotations(in the return value, in the variable assignment, ...). There are a lot of such config variables mypy to run in the lenient mode.In [8]: Question.objects.all()
Out[8]: <QuerySet [<Question: Question object (1)>,
<Question: Question object (2)>]>
In [9]: Question.objects.filter()
Out[9]: <QuerySet [<Question: Question object (1)>,
<Question: Question object (2)>]>
def filter_question(text: str) -> QuerySet[Question]:
return Question.objects.filter(
text__startswith=text)
def exclude_question(text: str) -> QuerySet[Question]:
return Question.objects.exclude(
text__startswith=text)
all, reverse, order_by, distinct, select_for_update, prefetch_related, ...
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
name = models.CharField(max_length=300)
pages = models.IntegerField()
# use integer field in production
price = models.DecimalField(max_digits=10,
decimal_places=2)
rating = models.FloatField()
publisher = models.ForeignKey(Publisher)
pubdate = models.DateField()
Publisher
model stores the data of the book publisher with name
as an explicit character field.Book
model contains six explicit model fields. pages
- Integer Fieldprice
- Decimal Field>>>def get_avg_price():
return Book.objects.all().aggregate(
avg_price=Avg("price"))
>>>print(get_avg_price())
{'avg_price': Decimal('276.666666666667')}
get_avg_price
returns the average price of all the books. avg_price is a Django query expression in the aggregate method. From the get_avg_price
function output, the output value is a dictionary.from decimal import Decimal
def get_avg_price() -> dict[str, Decimal]:
return Book.objects.all().aggregate(
avg_price=Avg("price"))
dictionary
. dict[str, Decimal]
is the return type annotation. The first type of argument(str
) in the dict specification is the dictionary's key's type. The second type of argument(Decimal
) is the value of the key, Decimal
.Annotates each object in the QuerySet
with the provided list of query expressions. An expression may be a simple value, a reference to a field on the model (or any related models), or an aggregate expression (averages, sums, etc.) that has been computed over the objects that are related to the objects in the QuerySet
.
def count_by_publisher():
return Publisher.objects.annotate(
num_books=Count("book"))
def print_pub(num_books=0):
if num_books > 0:
res = count_by_publisher().filter(
num_books__gt=num_books)
else:
res = count_by_publisher()
for item in res:
print(item.name, item.num_books)
count_by_publisher
function counts the books published by the publisher. The print_pub function
filters the publisher count based on the num_book function argument and prints the result.>>># after importing the function
>>>print_pub()
Penguin 2
vintage 1
print_pub
prints publication house name and their books count. Next is adding an annotation to both the function.from typing import TypedDict
from collections.abc import Iterable
class PublishedBookCount(TypedDict):
name: str
num_books: int
def count_by_publisher() ->
Iterable[PublishedBookCount]:
...
count_by_publisher
returns more than one value, and the result is iterable. TypedDict
is useful when the dictionary contents keys are known in advance. The attribute names of the class are the key names(should be a string), and the value type is an annotation to the key. count_by_publisher
's annotation is Iterable[PublishedBookCount]
.$# mypy output
scratch.py:46: error: Incompatible return value
type (got "QuerySet[Any]", expected
"Iterable[PublishedBookCount]")
return Publisher.objects.annotate(
num_books=Count("book"))
^
scratch.py:51: error:
"Iterable[PublishedBookCount]" has no attribute "filter"
res = count_by_publisher().filter(
num_books__gt=num_books)
.annotate
method returns QuerySet[Any]
whereas annotation says return type as Iterable[PublishedBookCount]
.print_pub
uses return value from count_by_publisher
to filter the values. Since the return value is iterable and the filter method is missing, mypy complains.def count_by_publisher() -> QuerySet[Publisher]:
...
def print_pub(num_books: int=0) -> None:
...
for item in res:
print(item.name, item.num_books)
count_by_publisher
to QuerySet[Publisher]
as suggested by mypy. Now the first error is fixed, but some other error.# mypy output
$mypy scratch.py
scratch.py:55: error: "Publisher" has
no attribute "num_books"
print(item.name, item.num_books)
num_books
attribute to the return QuerySet. The publisher model has one explicitly declared attribute name, and num_books
is nowhere declared, and mypy is complaining. from django_stubs_ext import WithAnnotations
class TypedPublisher(TypedDict):
num_books: int
def count_by_publisher() -> WithAnnotations[Publisher, TypedPublisher]:
...
WithAnnotation
takes two argument the model
and TypedDict
with on the fly fields.TypedPublisher
inside TYPE_CHECKING
block, which is only visible to mypy during static type-checking time. The TypedPublisher
inherits Publisher
model and declares the num_books
attribute as Django field, Then mypy will not complain about the missing attribute.from typing import TYPE_CHECKING
if TYPE_CHECKING:
class TypedPublisher(Publisher):
num_books = models.IntegerField()
class meta:
abstract = True
def count_by_publisher() -> QuerySet[TypedPublisher]:
return Publisher.objects.annotate(
num_books=Count("book"))
def print_pub(num_books: int=0) -> None:
if num_books > 0:
res = count_by_publisher().filter(
num_books__gt=num_books)
else:
res = count_by_publisher()
for item in res:
print(item.name, item.num_books)
from django.http import (HttpResponse,
HttpResponseNotFound)
# Create your views here.
# annotate the return value
def index(request):
return HttpResponse("hello world!")
def view_404_0(request):
return HttpResponseNotFound(
'<h1>Page not found</h1>')
from polls.views import *
from django.test import RequestFactory
def test_index():
request_factory = RequestFactory()
request = request_factory.post('/index')
index(request)
def test_view_404_0():
request_factory = RequestFactory()
request = request_factory.post('/404')
view_404_0(request)
$DJANGO_SETTINGS_MODULE="mysite.settings" PYTHONPATH='.' poetry run pytest -sv polls/tests.py --annotate-output=./annotations.json
--annotate-ouput
to store the inferred annotations.$cat annotations.json
[...
{
"path": "polls/views.py",
"line": 7,
"func_name": "index",
"type_comments": [
"(django.core.handlers.wsgi.WSGIRequest) ->
django.http.response.HttpResponse"
],
"samples": 1
},
{
"path": "polls/views.py",
"line": 10,
"func_name": "view_404_0",
"type_comments": [
"(django.core.handlers.wsgi.WSGIRequest) ->
django.http.response.HttpResponseNotFound"
],
"samples": 1
}
]
annotations.json
file contains the inferred annotations.$poetry run pyannotate --type-info ./annotations.json -w polls/views.py --py3
annotations.json
to the source code in pools/views.py
. --py3
flag indicates, the type-annotations should follow Python 3 syntax.from django.http import HttpResponse, HttpResponseNotFound
from django.core.handlers.wsgi import WSGIRequest
from django.http.response import HttpResponse
from django.http.response import HttpResponseNotFound
def index(request: WSGIRequest) -> HttpResponse:
return HttpResponse("hello world!")
def view_404_0(request: WSGIRequest) -> HttpResponseNotFound:
return HttpResponseNotFound('<h1>Page not found</h1>')
types
at test time, and runtime can be different. Example: Dummy Email Provider. That's what happened in the current case. Django tests don't use HTTPRequest, and the tests use WSGIRequest
the request argument type annotation is WSGIRequest.pyannotate
is better(run Django server as part of pyannotate) and infers the type correctly. Cup cake Photo by Alexandra Kusper on Unsplash
Tool Board Photo by Nina Mercado on Unsplash
Store Photo by Jan Antonin Kolar on Unsplash
Knot Photo by John Lockwood on Unsplash
Records Photo by Joseph Pearson on Unsplash
1/3. Blog Post of @pyconindia and @europython talk, Type Check your Django App is out. https://t.co/hAWhBljSYD #Python #Django
— kracekumar || கிரேஸ்குமார் (@kracetheking) September 24, 2021