A Pythonic implementation of Conway's Game of Life I've just finished implementing Conway's Game of Life in python-3.6:

import curses
import numpy as np
import time

from collections import defaultdict
from enum import Enum
from typing import List


class State(Enum):
    ''' enum class to represent possible cell states '''
    dead = 0
    alive = 1


class Board:
    ''' Board class to represent the game board '''
    def __init__(self, m : int, n : int, init : List[List[int]]):
        self.m = m   # the number of rows
        self.n = n   # the number of columns
        self.board_ = [
            [State(init[i][j]) for j in range(self.n)] for i in range(self.m)
        ]

    def __str__(self) -&gt; str:
        ''' return the __str__ representation of a Board object
            * represents a live cell, and a space represents a dead one
        '''
        return '\n'.join([
            ''.join(
                    ['*' if cell.value else ' ' for cell in row]
            ) for row in self.board_]
        )

    @property
    def population(self):
        ''' population — the number of live cells on the board '''
        return sum(cell.value for row in self.board_ for cell in row)

    def count_live_neighbours(self, x : int, y : int) -&gt; int:
        ''' count the live neighbours of a cell '''
        count = 0
        for i in range(x - 1, x + 2):
            for j in range(y - 1, y + 2):
                if (i == x and j == y) or i &lt; 0 or j &lt; 0:
                    continue
                # handle IndexErrors raised during invalid indexing operations 
                try:
                    count += self.board_[i][j].value
                except IndexError:
                    continue

        return count

    def next_cell_state(self, x : int, y : int) -&gt; State:
        count = self.count_live_neighbours(x, y)
        cur_state = self.board_[x][y]
        # determine the next state based on the current state and 
        # number of live neighbours 
        if count in {2, 3} and cur_state == State.alive:
            return cur_state
        elif count == 3 and cur_state == State.dead:
            return State.alive

        return State.dead

    def next_board_state(self) -&gt; List[List[State]]: 
        ''' return board configuration for the next state '''    
        return [
            [self.next_cell_state(i, j) for j in range(self.n)] for i in range(self.m)
        ]

    def advance_state(self):
        ''' update the board configuration with the config for the next state '''
        self.board_ = self.next_board_state()

    def has_live_cells(self) -&gt; bool:
        ''' return whether there are any live cells or not '''
        return any(cell.value for row in self.board_ for cell in row)


if __name__ == '__main__':
    arr = np.random.choice([0, 1], (20, 100), p=[0.90, 0.1]) 
    board = Board(arr.shape[0], arr.shape[1], init=arr.tolist())

    step = 0
    while board.has_live_cells():
        step += 1
        print(board)
        print('f{step} {board.population}')
        board.advance_state()

        time.sleep(.1)


To run the code, just invoke python3.6 conway.py. The board shape is fixed; but the configuration is randomly generated with NumPy.

My aim with this project was:


Implement a concise and pythonic version of the algorithm
Get better with Python's OOP constructs
Kill time :-)


There are a few redundant functions and properties which felt would be useful at some point of time.

My next step with this program would likely be to host it on a server and let people run it on their browser with a Step and Run button. This might influence how I'd need to structure my code.
Your code is pretty good, and there's not much I'd change. However I would change it in the following ways:


It's standard to use UPPER_SNAKE_CASE for Enum values.
It's preferred to use typing.Sequence over typing.List to annotate arguments.
When you have to comment what a variable is, it indicates you've chosen a bad name. Instead use rows over m.
Either put your comprehensions on one line or split them out.

# Current way
[
    [State(init[i][j]) for j in range(self.n)] for i in range(self.m)
]

# One line
[[State(init[i][j]) for j in range(self.n)] for i in range(self.m)]

# Multiple lines
[
    [
      State(init[i][j])
      for j in range(self.n)
    ]
    for i in range(self.m)
]

It's easier to read a comprehension in a function if you keep the brackets together.

# Current
return '\n'.join([
    ''.join(
            ['*' if cell.value else ' ' for cell in row]
    ) for row in self.board_]
)

# Together
return '\n'.join([
    ''.join(['*' if cell.value else ' '
             for cell in row])
        for row in self.board_])

# Together and multiple lines
return '\n'.join([
    ''.join([
        '*' if cell.value else ' '
        for cell in row
    ])
    for row in self.board_
])

You don't need to use the surrounding brackets if your comprehension is the only argument to a function. However, this may be a couple of u seconds slower, which shouldn't matter to most people.

return '\n'.join(
    ''.join(
        '*' if cell.value else ' '
        for cell in row
    )
    for row in self.board_
)

I'd make another function neighbours. I find this nicer, as finding the neighbors can be a little annoying, and take away from the actual algorithm.

I'd also make the algorithm use a premade list, that filters out x==i already.

NEIGHBOURS = [(i, j) for i in range(-1, 2) for j in range(-1, 2) if i or j]

def neighbours(self, x: int, y: int) -&gt; Iterator[Tuple[State, int, int]]:
    for i, j in ((x+i, y+j) for i, j in NEIGHBOURS):
        try:
            yield self.board_[i][j], i, j
        except IndexError:
            pass

def count_live_neighbours(self, x : int, y : int) -&gt; int:
    ''' count the live neighbours of a cell '''
    return sum(cell.value for cell, _, _ in self.neighbours(x, y))

Your next_cell_state function can be simplified.


Your two ifs return State.ALIVE.
They can be merged together.
You can return early if count is three.
You can simplify the rest of the checks.




def next_cell_state(self, x : int, y : int) -&gt; State:
    count = self.count_live_neighbours(x, y)
    if count == 3 or (count == 2 and self.board_[x][y] == State.ALIVE):
        return State.ALIVE
    return State.DEAD





import numpy as np
import time

from enum import Enum
from typing import List, Iterator, Sequence, Tuple


NEIGHBOURS = [(i, j) for i in range(-1, 2) for j in range(-1, 2) if i or j]


class State(Enum):
    ''' enum class to represent possible cell states '''
    DEAD = 0
    ALIVE = 1


class Board:
    ''' Board class to represent the game board '''
    def __init__(self, rows : int, columns : int, init : Sequence[Sequence[int]]):
        self.rows = rows   # the number of rows
        self.columns = columns   # the number of columns
        self.board_ = [
            [
                State(init[i][j])
                for j in range(self.columns)
            ]
            for i in range(self.rows)
        ]

    def __str__(self) -&gt; str:
        ''' return the __str__ representation of a Board object
            * represents a live cell, and a space represents a dead one
        '''
        return '\n'.join([
            ''.join([
                '*' if cell.value else ' '
                for cell in row
            ])
            for row in self.board_
        ])

    @property
    def population(self) -&gt; int:
        ''' population — the number of live cells on the board '''
        return sum(cell.value for row in self.board_ for cell in row)

    def has_live_cells(self) -&gt; bool:
        ''' return whether there are any live cells or not '''
        return any(cell.value for row in self.board_ for cell in row)

    def neighbours(self, x: int, y: int) -&gt; Iterator[Tuple[State, int, int]]:
        for i, j in ((x+i, y+j) for i, j in NEIGHBOURS):
            try:
                yield self.board_[i, j], i, j
            except IndexError:
                pass

    def count_live_neighbours(self, x : int, y : int) -&gt; int:
        ''' count the live neighbours of a cell '''
        return sum(cell.value for cell, _, _ in self.neighbours(x, y))

    def next_cell_state(self, x : int, y : int) -&gt; State:
        count = self.count_live_neighbours(x, y)
        if count == 3 or (count == 2 and self.board_[x][y] == State.ALIVE):
            return State.ALIVE
        return State.DEAD

    def next_board_state(self) -&gt; List[List[State]]: 
        ''' return board configuration for the next state '''
        return [
            [
                self.next_cell_state(i, j)
                for j in range(self.columns)
            ]
            for i in range(self.rows)
        ]

    def advance_state(self):
        ''' update the board configuration with the config for the next state '''
        self.board_ = self.next_board_state()


if __name__ == '__main__':
    arr = np.random.choice([0, 1], (20, 100), p=[0.90, 0.1]) 
    board = Board(arr.shape[0], arr.shape[1], init=arr.tolist())

    step = 0
    while board.has_live_cells():
        step += 1
        print(board)
        print('f{step} {board.population}')
        board.advance_state()

        time.sleep(.1)