Cellular automaton (Conway's Game of Life) in python As a practice exercise, I implemented a cellular automaton (Conway's Game of Life) in python (with help from this tutorial).

I am very curious, if this code follows common best practices and I am wondering how to improve the code in terms of performance and readability. 

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation

arr = []

size_grid_x = 6
size_grid_y = 6
universe = np.zeros((size_grid_x, size_grid_y))
new_universe = np.copy(universe)

beacon = [[1, 1, 0, 0],
          [1, 1, 0, 0],
          [0, 0, 1, 1],
          [0, 0, 1, 1]]

universe[1:5, 1:5] = beacon


def update(universe, x, y):
    X = size_grid_x
    Y = size_grid_y

    neighbors = lambda x, y: [(x2, y2) for x2 in range(x - 1, x + 2)
                              for y2 in range(y - 1, y + 2)
                              if (-1 &lt; x &lt;= X and
                                  -1 &lt; y &lt;= Y and
                                  (x != x2 or y != y2) and
                                  (0 &lt;= x2 &lt;= X) and
                                  (0 &lt;= y2 &lt;= Y))]

    num_neighbours = sum([universe[i] for i in neighbors(x, y)])
    new_val = universe[x, y]

    if universe[x, y] and not 2 &lt;= num_neighbours &lt;= 3:
        new_val = 0

    elif num_neighbours == 3:
        new_val = 1
    return new_val


for i in range(100):
    for x in range(5):
        for y in range(5):
            new_universe[x, y] = update(universe, x, y)

    universe[:, :] = new_universe[:, :]
    arr.append(np.copy(universe))

fig = plt.figure()
i = 0
im = plt.imshow(arr[0], animated=True)


def update_figure(*args):
    global i
    if i &lt; 99:
        i += 1
    else:
        i = 0
    im.set_array(arr[i])
    return im,


ani = animation.FuncAnimation(fig, update_figure, blit=True)
plt.show()
I'm on my phone, so I can't write anything too elaborate. I'll just make some notes:



I don't think you're making good use of functions here. For the most part, you have everything tucked inside of update. I think that function is far too large, and doing too much explicitly. I would, for example, create a function called should_live or something similar that is passed the cell's current state and number of neighbors, and returns if the cell should be alive the next tick or not.

You do break some of that functionality off into a local function neighbors, but I don't think it's ideal the way you've done it. I would make that an external function, and I would call it n_neighbors or count_neighbors or something similar to make it clearer that it's a function that counts neighbors, not a collection of neighbors. You could also clean up that long list comprehension guard by creating a is_inbounds function that checks if a point is i bounds or not. 

I'd also rename update, as is isn't actually updating anything. It's gathering information then returning a decision. 



I would not have code top-level like you have it here. Having the code run just because you loaded it will get annoying if you ever change your workflow to involve a REPL. I would tuck the routine into a main function, then maybe call that function from a if __name__ == "__main__" block.



In short, I'd break your code up more and be more cautious when it comes to naming.