Receive a list of names and return a list of users - HackerRank test
I would warn you from using such an approach - it may lead to buggy and unexpected results.Consider the following situation:

names = ['john', 'john', 'tom', 'john']
res1 = username_system(names)
print(res1)    # ['john', 'john1', 'tom', 'john2']

lst = ['a']
res2 = username_system(names, users=lst)
print(res2)   # ['a', 'john3', 'john4', 'tom1', 'john5']


Looking at the 2nd print result ...
Using a mutable data structures as function default arguments is considered as a fragile approach - Python "memorizes"(retains) that mutable arguments content/value between subsequent function's calls. Furthermore, you use 2 of such.Though in some cases it may be viable for defining a specific internal recursive functions - I'd suggest a more robust and faster approach.

As you're mentioned about "memoization", "to make it faster", "O(N)" here's a deterministic profiling stats for your initial function:

import cProfile

# def username_system(...)
names = ['john', 'john', 'tom', 'john']
cProfile.run('username_system(names)')


Output:

         27 function calls (23 primitive calls) in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 &lt;string&gt;:1(&lt;module&gt;)
      5/1    0.000    0.000    0.000    0.000 test.py:3(username_system)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        4    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        5    0.000    0.000    0.000    0.000 {method 'copy' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        4    0.000    0.000    0.000    0.000 {method 'keys' of 'dict' objects}
        4    0.000    0.000    0.000    0.000 {method 'remove' of 'list' objects}
        2    0.000    0.000    0.000    0.000 {method 'update' of 'dict' objects}


As you may observe even for that simple input list of 4 names username_system function is called 5 times.



Instead, we'll rely on supplementary collections.defaultdict object that conveniently provides the initial value.Then, traversing over a copy of initial user names only entries that occur more than 1 time (within a dict keys view) will be appended with incremented ordinal suffix:

from collections import defaultdict

def get_unique_usernames(user_names):
    d = defaultdict(int)
    uniq_unames = user_names[:]

    for i, name in enumerate(uniq_unames):
        if name in d:
           uniq_unames[i] += str(d[name])
        d[name] += 1

    return uniq_unames


if __name__ == "__main__": 
    names = ['john', 'john', 'tom', 'john']
    print(get_unique_usernames(names))    # ['john', 'john1', 'tom', 'john2']




Comparison of Time execution performance:

initial setup:

from timeit import timeit

names = ['john', 'john', 'tom', 'john']




username_system function:

print(timeit('username_system(names)', 'from __main__ import names, username_system', number=10000))
0.027410352995502762


get_unique_usernames function:

print(timeit('get_unique_usernames(names)', 'from __main__ import names, get_unique_usernames', number=10000))
0.013344291000976227