How to remove all instances of None from the end of a list?

Viewed 117

Python has a string method called rstrip():

>>> s = "hello world!!!"
>>> s.rstrip("!")
'hello world'

I want to implement similar functionality for a Python list. That is, I want to remove all instances of a given value from the end of a list. In this case, the value is None.

Here's some starting examples:

[1, 2, 3, None]
[1, 2, 3, None, None, None]
[1, 2, 3, None, 4, 5]
[1, 2, 3, None, None, 4, 5, None, None]

I want the end results to be:

[1, 2, 3]
[1, 2, 3]
[1, 2, 3, None, 4, 5]
[1, 2, 3, None, None, 4, 5]

Here's my solution so far:

while l[-1] is None:
    l.pop()
2 Answers

If you want to modify the list in-place, then your solution is good, just make sure you handle the case when the list is empty:

while l and l[-1] is None:
    l.pop()

If you want to compute a new list, you can adapt your solution into:

def stripNone(l):
    if not l:
        return []
    
    rlim = 0
    for x in reversed(l):
        if x is None:
            rlim += 1
        else:
            break
    
    return l[: len(l) - rlim]

There is also itertools.dropwhile, but you have to perform two reversals:

def stripNone(l):
    return list(dropwhile(lambda x: x is None, l[::-1]))[::-1]

Two more versions that also work for None-only lists:

while None in l[-1:]:
    l.pop()
for x in reversed(l):
    if x is not None:
        break
    l.pop()

Benchmarking some solutions on l = [None] * 10**3:

 83 us  stripNone1
137 us  stripNone2
 60 us  stripNone3
 42 us  stripNone3b
 53 us  stripNone4
 34 us  stripNone5
 19 us  stripNone6

Note that stripNone2 and stripNone6 have a small flaw: If there's an object among the trailine Nones that isn't None but claims to equal None, then it'll get removed. Such an object would be highly unusual, though. And perhaps one would actually want to remove such an object as well.

Benchmark code:

def stripNone1(l):
    while l and l[-1] is None:
        l.pop()

def stripNone2(l):
    while None in l[-1:]:
        l.pop()

def stripNone3(l):
    for x in reversed(l):
        if x is not None:
            break
        l.pop()

def stripNone3b(l):
    pop = l.pop
    for x in reversed(l):
        if x is not None:
            break
        pop()

def stripNone4(l):
    for i, x in enumerate(reversed(l), 1):
        if x is not None:
            del l[-i:]
            break

def stripNone5(l):
    pop = l.pop
    try:
        while (last := pop()) is None:
            pass
        l.append(last)
    except IndexError:
        pass

def stripNone6(l):
    while l:
        chunk = l[-32:]
        if chunk.count(None) < len(chunk):
            while l[-1] is None:
                l.pop()
            break
        del l[-32:]

from timeit import repeat
solutions = stripNone1, stripNone2, stripNone3, stripNone3b, stripNone4, stripNone5, stripNone6
for i in range(3):
    print(f'Round {i+1}:')
    for sol in solutions:
        ls = [[42] * head + [None] * tail
              for _ in range(5)
              for head in range(0, 2001, 200)
              for tail in range(0, 2001, 200)]
        number = len(ls) // 5
        ls = iter(ls)
        time = min(repeat(lambda: sol(next(ls)), number=number)) / number
        print(f'{int(time * 10**6):3d} us  {sol.__name__}')
Related