Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please see our video player troubleshooting guide to resolve the issue.

Introduce a Separate Validation Layer

00:00 Introduce a Separate Validation Layer. As with the grid, creating an instance of the GameState class should fail when the supplied combination of cells and the starting player’s mark don’t make sense.

00:13 For example, it’s currently possible to create an invalid game state that doesn’t reflect genuine gameplay. You can test this yourself. Start an interactive Python interpreter session in the virtual environment where you’d previously installed your library and run the code seen on-screen.

00:35 Here you initialize a new GameState using a grid comprising a syntactically correct string with the right characters and length. But such a cell combination is semantically incorrect because one player isn’t allowed to fill the entire grid with their mark. Because validating the game state is relatively involved, implementing it in the domain model would violate the single-responsibility principle and make your code less readable.

01:02 Validation belongs to a separate layer in your architecture, so you should keep the domain model and its validation logic in two different Python modules without mixing their code.

01:15 Go ahead and create two new files in your project: exceptions.py and validators.py. You’ll store various helper functions in validators.py and a few exception classes in the exceptions.py file to decouple game state validation from the model.

01:33 For improved code consistency, you can extract the grid validation that you defined earlier in the .__post_init__() method and move it into the newly created Python module and wrap it in a new function.

02:03 Note that you replace self.cells with grid.cells because you’re now referring to a grid instance through the function’s argument.

02:13 After extracting the grid validation logic, you should update the corresponding part in the grid model by delegating the validation to an appropriate abstraction.

02:26 First, you import the new helper function,

02:33 and then you call it in the grid’s .__post_init__() hook, which now uses a higher-level vocabulary to communicate its intent. Previously, some low-level details, such as the use of regular expressions, were leaking into the model, and it wasn’t immediately clear what the .__post_init__() method does.

02:51 Unfortunately, this change now creates the notorious circular-reference problem between the model and validator layers, which mutually depend on each other.

03:01 When you try to import Grid, you’ll get the error seen on-screen.

03:11 This is because Python reads the source code from top to bottom. As soon as it encounters an import statement, it will jump to the imported file and start reading it.

03:20 But in this case, the imported validators module wants to import the models module, which hasn’t been fully processed yet. This is a very common problem in Python when you start using type hints.

03:33 The only reason you’d need to import models is because of a type hint in your validating function. You could get away without the import statement by surrounding the type hint with quotes to make a forward declaration as seen earlier on in the course, but you’ll follow a different idiom this time.

03:50 You can combine the postponed evaluation of annotations with a special TYPE_CHECKING constant.

04:18 You import Grid conditionally. The TYPE_CHECKING constant is False at runtime, but third-party tools such as mypy will pretend it’s True when performing static type checking to allow the import statement to run.

04:31 However, because you no longer import the required type at runtime, you must now use forward declarations or take advantage of from __future__ import annotations, which will implicitly turn annotations into string literals.

04:49 Note that the __future__ import was originally intended to make the migration from Python 2 to Python 3 more seamless. Today, you can use it to enable various language features planned for future releases.

05:01 Once the feature becomes part of the standard Python distribution and you don’t need to support older language versions, you can remove the import. You can read more about this behavior at the link to the Python documentation seen on-screen.

05:18 The import statement that previously generated an error now works correctly.

05:26 With all of this plumbing in place, you are finally ready to constrain the game state to comply with the tic-tac-toe rules. In the next section of the course, you’ll add a few GameState validation functions to the new validators module.

Become a Member to join the conversation.