Since python 3.6, immutable object now can be defined with named tuple (with optional/default variable in 3.6.1). This brings python syntax of defining data model close to my favorite Scala case class (even better if Python has case pattern matching).

from typing import NamedTuple

class Article(NamedTuple):
    title: str
    author: str
    optional_meta: dict = None

Before python 3.6, NamedTuple syntax is quite taxing to write

from typing import NamedTuple

class Article(NamedTuple('Article', [
    ('title', str),
    ('author', str),
    ('optional_meta', dict),
    ])):
    # Potentially breaking typing hints for IDE, also *args
    def __new__(cls, optional_meta=None, *args, **kwargs):
        return super(Article, cls).__new__(cls, optional_meta=None, *args, **kwargs)

Differences for swapping class with NamedTuple:

For class that involved encoding to json, the self.__dict__ becomes self._asdict()

import json
class _Encoder(json.JSONEncoder):
    def default(self, o):
        # return o.__dict__
        return o._asdict()

print(json.dumps(res, cls=_Encoder, sort_keys=True, indent=4))

Copy constructing, becomes a lot easier with self._replace(author='Kelvin').

Example of sharing states of immutable without side effects by copying.

import numpy as np
from typing import NamedTuple, Iterable
"""
Tree search TicTacToe
"""
class GameState(NamedTuple):
    game_id: int
    player_turn: int
    board: np.ndarray

def available_moves(state) -> np.ndarray:
    return np.nonzero(state.board == 0)[0]

def move(state, board_idx:int) -> GameState:
    return state._replace(
        player_turn=(2 if state.player_turn==1 else 1),
        board=state.board + ((np.arange(state.board.shape[0]) == board_idx) * state.player_turn)
    )

def tree_search(state=GameState(123, 1, np.zeros(9)), random_size=2):
    ams = available_moves(state)
    if ams.any():
        for am in np.random.choice(ams, size=random_size):
            # Create a copy of the state instead of mutating the shared reference
            tree_search(move(state, am))
    else:
        print(state)

tree_search()