Codewars: Make a spiral
Without changing your algorithm we can make your code simpler in a few places. I'd say the algorithm is quite sensible, I would've chosen the same approach.

Comments and docstrings
Providing some comments here and there and explaining the general algorithmic approach in a docstring would be good start for increasing readability. In particular, it's not intuitively obvious why while change_directions &lt; size:
is the correct approach.

Nested functions
I find nested functions to be relatively hard to read. Especially when they start using variables from the outer scope (the outer function), thereby mixing namespaces. Personally, I generally try to avoid them. In the case of check_collision I'd say it might improve readability as it reduces the maximum indentation level. That's generally a good thing, since every indentation level is another layer of context to keep track of.

get_change_values()
We can easily get rid of this function by taking a closer look at the underlying logic. We don't actually need to pass around direction strings, since we always follow the same pattern RIGHT -&gt; DOWN -&gt; LEFT -&gt; UP -&gt; RIGHT -&gt; .... The itertools module from the standard library provides cycle, which basically provides an endless iterator for a given iterable (example from the docs: cycle('ABCD') --&gt; A B C D A B C D A B C D ...). So we might want to cycle(['right', 'down', 'left', 'up']). But since we only need the strings to get the corresponding change_x, change_y values, we can replace them by those values directly:
directions = cycle(((1, 0), (0, 1), (-1, 0), (0, -1)))

We now replace
direction = directions[change_directions % 4]
change_x, change_y = get_change_values()

by
change_x, change_y = next(directions)

and get rid of get_change_values().

0 &lt;= variable &lt; limit
Conditions that follow the pattern 0 &lt;= variable &lt; limit can be replaced by variable in range(limit). I personally find this to be more readable, but it's not always preferable and up to personal preference.

check_collision()
This function can be simplified to the point where we could directly put it into our nested while loop. This is a valid option here. But as mentioned above you might want to keep it in a seperate function to reduce the maximum indentation level.
We don't actually need to check for the direction:
if direction in ('right', 'left'):
    if spiral[y][x + change_x * 2]:
        return True
elif direction in ('down', 'up'):
    if spiral[y + change_y * 2][x]:
        return True

if we simply check
if spiral[y + change_y * 2][x + change_x * 2] == 1:
    return True

We can also directly return:
def check_collision() -&gt; bool:
    if y + change_y * 2 in range(size) and x + change_x * 2 in range(size):
        return spiral[y + change_y * 2][x + change_x * 2] == 1
    
    return False

I find exit conditions to generally improve code readability as well. They're usually closer to the reader's thought process (&quot;If the step is out of bounds there's no collision, so we don't need to ckeck anything else. Otherwise check if the step leads to a 1.&quot;):
def check_collision() -&gt; bool:
    if y + change_y * 2 not in range(size) or x + change_x * 2 not in range(size):
        return False
    
    return spiral[y + change_y * 2][x + change_x * 2] == 1

Both options are already pretty clean. I personally prefer this branchless approach:
def check_collision() -&gt; bool:
    step_inside_bounds = y + change_y * 2 in range(size) and x + change_x * 2 in range(size)
    return step_inside_bounds and spiral[y + change_y * 2][x + change_x * 2] == 1

This works because and is evaluated lazily, so if step_inside_bounds is False, the rest of the condition is not evaluated.

spiral initialization
Instead of
spiral = [[0 for _ in range(size)] for _ in range(size)]

we can initialize spiral like this
spiral = [[0] * size for _ in range(size)]

This again is just another way to do things, and I wouldn't say it's definitely better for multi-dimensional lists. For one-dimensional lists, I would prefer the suggested syntax. Compare these examples:
my_list = [0 for _ in range(my_length)]
my_list = [0] * my_length


Complete code
I renamed change_directions to direction_changes and check_collision to collision_in_two_steps. I also added type hints (PEP 484).
from itertools import cycle


def spiralize(size: int) -&gt; list[list[int]]:
    def collision_in_two_steps() -&gt; bool:
        step_inside_bounds = y + change_y * 2 in range(size) and x + change_x * 2 in range(size)
        return step_inside_bounds and spiral[y + change_y * 2][x + change_x * 2] == 1

    directions = cycle(((1, 0), (0, 1), (-1, 0), (0, -1)))

    spiral = [[0] * size for _ in range(size)]
    x, y = 0, 0
    direction_changes = 0

    while direction_changes &lt; size:
        change_x, change_y = next(directions)

        while y + change_y in range(size) and x + change_x in range(size):
            spiral[y][x] = 1

            if collision_in_two_steps():
                break

            y += change_y
            x += change_x

        direction_changes += 1

    return spiral