Python algorithm to return all two-tuple permutations of numbers up to n
I'd say your code is well-structured and easy to read. The approach however can be simplified using the itertools module from the standard library. itertools provides useful implementations for things like combinations, permutations, cartesian product and a lot more.
Here's the simplest and fastest approach I could come up with, that is equivalent to your implementation:
from itertools import product

def valid_tuple(number_pair: tuple) -&gt; bool:
    a, b = number_pair
    return abs(b) &lt;= abs(a)


def get_values_product(n: int) -&gt; list:
    out = product(range(-n, n + 1), repeat=2)
    out = filter(valid_tuple, out)
    return sorted(out, key=lambda t: abs(t[0]))

It's usually faster to use functions that are built-in or included in standard library modules. itertools.product returns the cartesian product of input iterables, which is exactly what we want here. On my machine, this takes about half the time of your original implementation.

As you need sorted output I don't think there's a way to significantly reduce the space complexity. If you didn't need the output to be sorted, you could turn it into a generator, which is really memory-efficient.
Edit:
Here's how you can get the same desired sorted output while optimising space complexity:
from itertools import product, chain
from typing import Generator

def get_values_generator(n: int) -&gt; Generator:
    if n &lt; 0:
        return

    numbers = chain(*zip(range(n + 1), range(-1, -n - 1, -1)), [n])

    pairs = product(numbers, repeat=2)

    yield from filter(valid_tuple, pairs)

This works because we already pre-sort the list of numbers before calculating the cartesian product. This implementation returns a generator object, i.e. an iterator instead of a list. This means you might have to make some slight adjustments to the code that uses the return value of get_values.
Here's a comparison of the memory used by the return values of both approaches:
num = 50

sorted_list = get_values(num)
generator_obj = get_values_generator(num)

print(sys.getsizeof(sorted_list))
print(sys.getsizeof(generator_obj))

prints
&gt; 85176
&gt; 112

When changing num = 500, we get:
&gt; 8448728
&gt; 112

As you can see, the memory used by the generator object is constant at 112 bytes.
Here's some validation regarding the correctness of our result:
print(&quot;Same elements:\t&quot;, set(sorted_list) == set(generated_list))
print(&quot;Same length:\t&quot;, len(sorted_list) == len(generated_list))
print(&quot;No duplicates:\t&quot;, len(generated_list) == len(set(generated_list)))

print(&quot;Sorted:\t\t\t&quot;, all(abs(x[0]) &lt;= abs(y[0]) for x, y in zip(generated_list, generated_list[1:])))

We get the same output for negative and positive numbers, as well as zero:
&gt; Same elements:     True
&gt; Same length:       True
&gt; No duplicates:     True
&gt; Sorted:            True