diff --git a/15-more-types/cafeteria/cafeteria.py b/15-more-types/cafeteria/cafeteria.py
new file mode 100644
index 0000000..8c88a92
--- /dev/null
+++ b/15-more-types/cafeteria/cafeteria.py
@@ -0,0 +1,41 @@
+from typing import TypeVar, Generic
+
+
+class Beverage:
+ """Any beverage"""
+
+
+class Juice(Beverage):
+ """Any fruit juice"""
+
+
+class OrangeJuice(Juice):
+ """Delicious juice Brazilian oranges"""
+
+
+class Coak(Beverage):
+ """Secret formula with lots of sugar"""
+
+
+BeverageT = TypeVar('BeverageT', bound=Beverage)
+JuiceT = TypeVar('JuiceT', bound=Juice)
+
+
+class BeverageDispenser(Generic[BeverageT]):
+
+ beverage: BeverageT
+
+ def __init__(self, beverage: BeverageT) -> None:
+ self.beverage = beverage
+
+ def dispense(self) -> BeverageT:
+ return self.beverage
+
+
+class JuiceDispenser(BeverageDispenser[JuiceT]):
+ pass
+
+
+class Cafeteria:
+ def __init__(self, dispenser: BeverageDispenser[JuiceT]):
+ self.dispenser = dispenser
diff --git a/15-more-types/cafeteria/cafeteria_demo.py b/15-more-types/cafeteria/cafeteria_demo.py
new file mode 100644
index 0000000..d93636b
--- /dev/null
+++ b/15-more-types/cafeteria/cafeteria_demo.py
@@ -0,0 +1,23 @@
+from cafeteria import (
+ Cafeteria,
+ BeverageDispenser,
+ JuiceDispenser,
+ Juice,
+ OrangeJuice,
+ Coak,
+)
+
+orange = OrangeJuice()
+
+orange_dispenser: JuiceDispenser[OrangeJuice] = JuiceDispenser(orange)
+
+juice: Juice = orange_dispenser.dispense()
+
+soda = Coak()
+
+## Value of type variable "JuiceT" of "JuiceDispenser" cannot be "Coak"
+# soda_dispenser = JuiceDispenser(soda)
+
+soda_dispenser = BeverageDispenser(soda)
+
+arnold_hall = Cafeteria(soda_dispenser)
diff --git a/15-more-types/collections_variance.py b/15-more-types/collections_variance.py
new file mode 100644
index 0000000..12fb9dc
--- /dev/null
+++ b/15-more-types/collections_variance.py
@@ -0,0 +1,16 @@
+from collections.abc import Collection, Sequence
+
+col_int: Collection[int]
+
+seq_int: Sequence[int] = (1, 2, 3)
+
+## Incompatible types in assignment
+## expression has type "Collection[int]"
+## variable has type "Sequence[int]"
+# seq_int = col_int
+
+col_int = seq_int
+
+## List item 0 has incompatible type "float"
+## expected "int"
+# col_int = [1.1]
diff --git a/15-type-hints/erp.py b/15-more-types/erp.py
similarity index 100%
rename from 15-type-hints/erp.py
rename to 15-more-types/erp.py
diff --git a/15-type-hints/erp_test.py b/15-more-types/erp_test.py
similarity index 94%
rename from 15-type-hints/erp_test.py
rename to 15-more-types/erp_test.py
index 5e8c067..9abdc86 100644
--- a/15-type-hints/erp_test.py
+++ b/15-more-types/erp_test.py
@@ -1,5 +1,4 @@
-import random
-from typing import Iterable, TYPE_CHECKING, List
+from typing import TYPE_CHECKING
from erp import EnterpriserRandomPopper
import randompop
@@ -9,7 +8,6 @@ def test_issubclass() -> None:
assert issubclass(EnterpriserRandomPopper, randompop.RandomPopper)
-
def test_isinstance_untyped_items_argument() -> None:
items = [1, 2, 3]
popper = EnterpriserRandomPopper(items) # [int] is not required
diff --git a/15-more-types/gen_contra.py b/15-more-types/gen_contra.py
new file mode 100644
index 0000000..a8ed9a9
--- /dev/null
+++ b/15-more-types/gen_contra.py
@@ -0,0 +1,59 @@
+"""
+In ``Generator[YieldType, SendType, ReturnType]``,
+``SendType`` is contravariant.
+The other type variables are covariant.
+
+This is how ``typing.Generator`` is declared::
+
+ class Generator(Iterator[T_co], Generic[T_co, T_contra, V_co]):
+
+(from https://docs.python.org/3/library/typing.html#typing.Generator)
+
+"""
+
+from typing import Generator
+
+
+# Generator[YieldType, SendType, ReturnType]
+
+def gen_float_take_int() -> Generator[float, int, str]:
+ received = yield -1.0
+ while received:
+ received = yield float(received)
+ return 'Done'
+
+
+def gen_float_take_float() -> Generator[float, float, str]:
+ received = yield -1.0
+ while received:
+ received = yield float(received)
+ return 'Done'
+
+
+def gen_float_take_complex() -> Generator[float, complex, str]:
+ received = yield -1.0
+ while received:
+ received = yield abs(received)
+ return 'Done'
+
+# Generator[YieldType, SendType, ReturnType]
+
+g0: Generator[float, float, str] = gen_float_take_float()
+
+g1: Generator[complex, float, str] = gen_float_take_float()
+
+## Incompatible types in assignment
+## expression has type "Generator[float, float, str]"
+## variable has type "Generator[int, float, str]")
+# g2: Generator[int, float, str] = gen_float_take_float()
+
+
+# Generator[YieldType, SendType, ReturnType]
+
+g3: Generator[float, int, str] = gen_float_take_float()
+
+## Incompatible types in assignment
+## expression has type "Generator[float, float, str]"
+## variable has type "Generator[float, complex, str]")
+## g4: Generator[float, complex, str] = gen_float_take_float()
+
diff --git a/15-more-types/petbox/petbox.py b/15-more-types/petbox/petbox.py
new file mode 100644
index 0000000..04b72ea
--- /dev/null
+++ b/15-more-types/petbox/petbox.py
@@ -0,0 +1,47 @@
+from typing import TypeVar, Generic, Any
+
+
+class Pet:
+ """Domestic animal kept for companionship."""
+
+
+class Dog(Pet):
+ """Canis familiaris"""
+
+
+class Cat(Pet):
+ """Felis catus"""
+
+
+class Siamese(Cat):
+ """Cat breed from Thailand"""
+
+
+T = TypeVar('T')
+
+
+class Box(Generic[T]):
+ def put(self, item: T) -> None:
+ self.contents = item
+
+ def get(self) -> T:
+ return self.contents
+
+
+T_contra = TypeVar('T_contra', contravariant=True)
+
+
+class InBox(Generic[T_contra]):
+ def put(self, item: T) -> None:
+ self.contents = item
+
+
+T_co = TypeVar('T_co', covariant=True)
+
+
+class OutBox(Generic[T_co]):
+ def __init__(self, contents: Any):
+ self.contents = contents
+
+ def get(self) -> Any:
+ return self.contents
diff --git a/15-more-types/petbox/petbox_demo.py b/15-more-types/petbox/petbox_demo.py
new file mode 100644
index 0000000..d09cab4
--- /dev/null
+++ b/15-more-types/petbox/petbox_demo.py
@@ -0,0 +1,42 @@
+from typing import TYPE_CHECKING
+
+from petbox import *
+
+
+cat_box: Box[Cat] = Box()
+
+si = Siamese()
+
+cat_box.put(si)
+
+animal = cat_box.get()
+
+#if TYPE_CHECKING:
+# reveal_type(animal) # Revealed: petbox.Cat*
+
+
+################### Covariance
+
+out_box: OutBox[Cat] = OutBox(Cat())
+
+out_box_si: OutBox[Siamese] = OutBox(Siamese())
+
+## Incompatible types in assignment
+## expression has type "OutBox[Cat]"
+# variable has type "OutBox[Siamese]"
+# out_box_si = out_box
+
+out_box = out_box_si
+
+################### Contravariance
+
+in_box: InBox[Cat] = InBox()
+
+in_box_si: InBox[Siamese] = InBox()
+
+in_box_si = in_box
+
+## Incompatible types in assignment
+## expression has type "InBox[Siamese]"
+## variable has type "InBox[Cat]"
+# in_box = in_box_si
diff --git a/15-type-hints/randompick_generic.py b/15-more-types/randompick_generic.py
similarity index 100%
rename from 15-type-hints/randompick_generic.py
rename to 15-more-types/randompick_generic.py
diff --git a/15-type-hints/randompick_generic_test.py b/15-more-types/randompick_generic_test.py
similarity index 97%
rename from 15-type-hints/randompick_generic_test.py
rename to 15-more-types/randompick_generic_test.py
index 07ebdc4..d642b26 100644
--- a/15-type-hints/randompick_generic_test.py
+++ b/15-more-types/randompick_generic_test.py
@@ -4,7 +4,7 @@ from typing import Iterable, TYPE_CHECKING
from randompick_generic import GenericRandomPicker
-class LottoPicker():
+class LottoPicker:
def __init__(self, items: Iterable[int]) -> None:
self._items = list(items)
random.shuffle(self._items)
diff --git a/15-type-hints/randompop.py b/15-more-types/randompop.py
similarity index 59%
rename from 15-type-hints/randompop.py
rename to 15-more-types/randompop.py
index 35cad38..cf9c811 100644
--- a/15-type-hints/randompop.py
+++ b/15-more-types/randompop.py
@@ -1,4 +1,4 @@
-from typing import Protocol, TypeVar, runtime_checkable, Any
+from typing import Protocol, runtime_checkable, Any
@runtime_checkable
diff --git a/15-type-hints/randompop_test.py b/15-more-types/randompop_test.py
similarity index 96%
rename from 15-type-hints/randompop_test.py
rename to 15-more-types/randompop_test.py
index 0b0f317..7cce7f6 100644
--- a/15-type-hints/randompop_test.py
+++ b/15-more-types/randompop_test.py
@@ -3,7 +3,7 @@ import random
from typing import Any, Iterable, TYPE_CHECKING
-class SimplePopper():
+class SimplePopper:
def __init__(self, items: Iterable) -> None:
self._items = list(items)
random.shuffle(self._items)
diff --git a/15-more-types/typeddict/books.py b/15-more-types/typeddict/books.py
new file mode 100644
index 0000000..5a0bc82
--- /dev/null
+++ b/15-more-types/typeddict/books.py
@@ -0,0 +1,32 @@
+# tag::BOOKDICT[]
+from typing import TypedDict, List
+import json
+
+class BookDict(TypedDict):
+ isbn: str
+ title: str
+ authors: List[str]
+ pagecount: int
+# end::BOOKDICT[]
+
+# tag::TOXML[]
+AUTHOR_EL = '{}'
+
+def to_xml(book: BookDict) -> str: # <1>
+ elements: List[str] = [] # <2>
+ for key, value in book.items():
+ if isinstance(value, list): # <3>
+ elements.extend(
+ AUTHOR_EL.format(n) for n in value) # <4>
+ else:
+ tag = key.upper()
+ elements.append(f'<{tag}>{value}{tag}>')
+ xml = '\n\t'.join(elements)
+ return f'\n\t{xml}\n'
+# end::TOXML[]
+
+# tag::FROMJSON[]
+def from_json(data: str) -> BookDict:
+ whatever: BookDict = json.loads(data) # <1>
+ return whatever # <2>
+# end::FROMJSON[]
\ No newline at end of file
diff --git a/15-more-types/typeddict/books_any.py b/15-more-types/typeddict/books_any.py
new file mode 100644
index 0000000..49a544e
--- /dev/null
+++ b/15-more-types/typeddict/books_any.py
@@ -0,0 +1,32 @@
+# tag::BOOKDICT[]
+from typing import TypedDict, List
+import json
+
+class BookDict(TypedDict):
+ isbn: str
+ title: str
+ authors: List[str]
+ pagecount: int
+# end::BOOKDICT[]
+
+# tag::TOXML[]
+AUTHOR_EL = '{}'
+
+def to_xml(book: BookDict) -> str: # <1>
+ elements: List[str] = [] # <2>
+ for key, value in book.items():
+ if isinstance(value, list): # <3>
+ elements.extend(AUTHOR_EL.format(n)
+ for n in value)
+ else:
+ tag = key.upper()
+ elements.append(f'<{tag}>{value}{tag}>')
+ xml = '\n\t'.join(elements)
+ return f'\n\t{xml}\n'
+# end::TOXML[]
+
+# tag::FROMJSON[]
+def from_json(data: str) -> BookDict:
+ whatever = json.loads(data) # <1>
+ return whatever # <2>
+# end::FROMJSON[]
diff --git a/15-more-types/typeddict/demo_books.py b/15-more-types/typeddict/demo_books.py
new file mode 100644
index 0000000..5203acb
--- /dev/null
+++ b/15-more-types/typeddict/demo_books.py
@@ -0,0 +1,20 @@
+from books import BookDict
+from typing import TYPE_CHECKING
+
+def demo() -> None: # <1>
+ book = BookDict( # <2>
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478
+ )
+ authors = book['authors'] # <3>
+ if TYPE_CHECKING: # <4>
+ reveal_type(authors) # <5>
+ authors = 'Bob' # <6>
+ book['weight'] = 4.2
+ del book['title']
+
+
+if __name__ == '__main__':
+ demo()
diff --git a/15-more-types/typeddict/demo_not_book.py b/15-more-types/typeddict/demo_not_book.py
new file mode 100644
index 0000000..7bf0711
--- /dev/null
+++ b/15-more-types/typeddict/demo_not_book.py
@@ -0,0 +1,23 @@
+from books import to_xml, from_json
+from typing import TYPE_CHECKING
+
+def demo() -> None:
+ NOT_BOOK_JSON = """
+ {"title": "Andromeda Strain",
+ "flavor": "pistachio",
+ "authors": true}
+ """
+ not_book = from_json(NOT_BOOK_JSON) # <1>
+ if TYPE_CHECKING: # <2>
+ reveal_type(not_book)
+ reveal_type(not_book['authors'])
+
+ print(not_book) # <3>
+ print(not_book['flavor']) # <4>
+
+ xml = to_xml(not_book) # <5>
+ print(xml) # <6>
+
+
+if __name__ == '__main__':
+ demo()
diff --git a/15-more-types/typeddict/test_books.py b/15-more-types/typeddict/test_books.py
new file mode 100644
index 0000000..fc9d245
--- /dev/null
+++ b/15-more-types/typeddict/test_books.py
@@ -0,0 +1,112 @@
+import json
+from typing import cast
+
+from books import BookDict, to_xml, from_json
+
+XML_SAMPLE = """
+
+\t0134757599
+\tRefactoring, 2e
+\tMartin Fowler
+\tKent Beck
+\t478
+
+""".strip()
+
+
+# using plain dicts
+
+def test_1() -> None:
+ xml = to_xml({
+ 'isbn': '0134757599',
+ 'title': 'Refactoring, 2e',
+ 'authors': ['Martin Fowler', 'Kent Beck'],
+ 'pagecount': 478,
+ })
+ assert xml == XML_SAMPLE
+
+def test_2() -> None:
+ xml = to_xml(dict(
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478))
+ assert xml == XML_SAMPLE
+
+def test_5() -> None:
+ book_data: BookDict = dict(
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478
+ )
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
+
+def test_6() -> None:
+ book_data = dict(
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478
+ )
+ xml = to_xml(cast(BookDict, book_data)) # cast needed
+ assert xml == XML_SAMPLE
+
+def test_4() -> None:
+ xml = to_xml(BookDict(
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478))
+ assert xml == XML_SAMPLE
+
+def test_7() -> None:
+ book_data = BookDict(
+ isbn='0134757599',
+ title='Refactoring, 2e',
+ authors=['Martin Fowler', 'Kent Beck'],
+ pagecount=478
+ )
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
+
+def test_8() -> None:
+ book_data: BookDict = {
+ 'isbn': '0134757599',
+ 'title': 'Refactoring, 2e',
+ 'authors': ['Martin Fowler', 'Kent Beck'],
+ 'pagecount': 478,
+ }
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
+
+BOOK_JSON = """
+ {"isbn": "0134757599",
+ "title": "Refactoring, 2e",
+ "authors": ["Martin Fowler", "Kent Beck"],
+ "pagecount": 478}
+"""
+
+def test_load_book_0() -> None:
+ book_data: BookDict = json.loads(BOOK_JSON) # typed var
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
+
+def test_load_book() -> None:
+ book_data = from_json(BOOK_JSON)
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
+
+
+NOT_BOOK_JSON = """
+ {"isbn": 3.141592653589793
+ "title": [1, 2, 3],
+ "authors": ["Martin Fowler", "Kent Beck"],
+ "flavor": "strawberry"}
+"""
+
+def test_load_not_book() -> None:
+ book_data: BookDict = json.loads(BOOK_JSON) # typed var
+ xml = to_xml(book_data)
+ assert xml == XML_SAMPLE
diff --git a/15-more-types/typeddict/test_books_check_fails.py b/15-more-types/typeddict/test_books_check_fails.py
new file mode 100644
index 0000000..6166f97
--- /dev/null
+++ b/15-more-types/typeddict/test_books_check_fails.py
@@ -0,0 +1,20 @@
+from books import BookDict, to_xml
+
+XML_SAMPLE = """
+
+\t0134757599
+\tRefactoring, 2e
+\tMartin Fowler
+\tKent Beck
+\t478
+
+""".strip()
+
+def test_3() -> None:
+ xml = to_xml(BookDict(dict([ # Expected keyword arguments, {...}, or dict(...) in TypedDict constructor
+ ('isbn', '0134757599'),
+ ('title', 'Refactoring, 2e'),
+ ('authors', ['Martin Fowler', 'Kent Beck']),
+ ('pagecount', 478),
+ ])))
+ assert xml == XML_SAMPLE
diff --git a/README.md b/README.md
index 56e7420..d0b57b3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Fluent Python 2e example code
-Example code for the book **Fluent Python, 2nd edition** by Luciano Ramalho (O'Reilly, 2021).
+Example code for the book **Fluent Python, 2nd edition** by Luciano Ramalho (O'Reilly, 2020).
> **BEWARE**: This is a work in progress!
>
@@ -14,40 +14,40 @@ Example code for the book **Fluent Python, 2nd edition** by Luciano R
All chapters are undergoing review and updates, including significant rewrites in the chapters about concurrency in **Part V**.
-New chapters in **Fluent Python 2nd edition** are marked with 🆕.
+New chapters in **Fluent Python 2e** are marked with 🆕.
-🚨 This table of contents is subject to change at any time until the book goes to the printer.
+🚨 This table of contents is subject to change at any time until the book goes to the printer.
-Part / Chapter #|Title|Directory|1st edition Chapter #
----:|---|---|:---:
+Part / Chapter #|Title|Directory|Notebook|1st ed. Chapter #
+---:|---|---|---|:---:
**I – Prologue**|
-1|The Python Data Model|[01-data-model](01-data-model)|1
+1|The Python Data Model|[01-data-model](01-data-model)|[data-model.ipynb](01-data-model/data-model.ipynb)|1
**II – Data Structures**|
-2|An Array of Sequences|[02-array-seq](02-array-seq)|2
-3|Dictionaries and Sets|[03-dict-set](03-dict-set)|3
-4|Text versus Bytes|[04-text-byte](04-text-byte)|4
-5|Record-like Data Structures|[05-record-like](05-record-like)|🆕
-6|Object References, Mutability, and Recycling|[06-obj-ref](06-obj-ref)|8
+2|An Array of Sequences|[02-array-seq](02-array-seq)|[array-seq.ipynb](02-array-seq/array-seq.ipynb)|2
+3|Dictionaries and Sets|[03-dict-set](03-dict-set)||3
+4|Text versus Bytes|[04-text-byte](04-text-byte)||4
+🆕 5|Record-like Data Structures|[05-record-like](05-record-like)||–
+6|Object References, Mutability, and Recycling|[06-obj-ref](06-obj-ref)||8
**III – Functions as Objects**|
-7|First-Class Funcions|[07-1class-func](07-1class-func)|5
-8|Type Hints in Function Definitions|[08-def-type-hints](08-def-type-hints)|🆕
-9|Function Decorators and Closures|[09-closure-deco](09-closure-deco)|7
-10|Design Patterns with First-Class Functions|[10-dp-1class-func](10-dp-1class-func)|6
+7|First-Class Funcions|[07-1class-func](07-1class-func)||5
+🆕 8|Type Hints in Function Definitions|[08-def-type-hints](08-def-type-hints)||–
+9|Function Decorators and Closures|[09-closure-deco](09-closure-deco)||7
+10|Design Patterns with First-Class Functions|[10-dp-1class-func](10-dp-1class-func)||6
**IV – Object-Oriented Idioms**|
-11|A Pythonic Object|[11-pythonic-obj](11-pythonic-obj)|9
-12|Sequence Hacking, Hashing, and Slicing|[12-seq-hacking](12-seq-hacking)|10
-13|Interfaces, Protocols, and ABCs|[13-protocl-abc](13-protocol-abc)|11
-14|Inheritance: For Good or For Worse|[14-inheritance](14-inheritance)|12
-15|More About Type Hints|15-more-typing|🆕
-16|Operator Overloading: Doing It Right|[16-op-overloading](16-op-overloading)|13
+11|A Pythonic Object|[11-pythonic-obj](11-pythonic-obj)||9
+12|Sequence Hacking, Hashing, and Slicing|[12-seq-hacking](12-seq-hacking)||10
+13|Interfaces, Protocols, and ABCs|[13-protocl-abc](13-protocol-abc)||11
+14|Inheritance: For Good or For Worse|[14-inheritance](14-inheritance)||12
+🆕 15|More About Type Hints|[15-more-types](15-more-types)||–
+16|Operator Overloading: Doing It Right|[16-op-overloading](16-op-overloading)||13
**V – Control Flow**|
-17|Iterables, Iterators, and Generators|[17-it-generator](17-it-generator)|14
-18|Context Managers and else Blocks|[18-context-mngr](18-context-mngr)|15
-19|Classic Coroutines|[19-coroutine](19-coroutine)|16
-20|Concurrency Models in Python|[20-concurrency](20-concurrency)|🆕
-21|Concurrency with Futures|[21-futures](21-futures)|17
-22|Asynchronous Programming|[22-async](22-async)|18
+17|Iterables, Iterators, and Generators|[17-it-generator](17-it-generator)||14
+18|Context Managers and else Blocks|[18-context-mngr](18-context-mngr)||15
+19|Classic Coroutines|[19-coroutine](19-coroutine)||16
+🆕 20|Concurrency Models in Python|[20-concurrency](20-concurrency)||-
+21|Concurrency with Futures|[21-futures](21-futures)||17
+22|Asynchronous Programming|[22-async](22-async)||18
**VI – Metaprogramming**|
-23|Dynamic Attributes and Properties|[23-dyn-attr-prop](23-dyn-attr-prop)|19
-24|Attribute Descriptors|[24-descriptor](24-descriptor)|20
-25|Class Metaprogramming|[25-class-metaprog](25-class-metaprog)|21
+23|Dynamic Attributes and Properties|[22-dyn-attr-prop](22-dyn-attr-prop)||19
+24|Attribute Descriptors|[23-descriptor](23-descriptor)||20
+25|Class Metaprogramming|[24-class-metaprog](24-class-metaprog)||21