Find Todos by Line Number (or any other reference) in Python - Revised as functions
Overall, pretty good job.  Here are some observations:

_get_files

path + f"/**/*{ext}"


Might cause trouble if path ends in a / and may be unsafe.  If a user inputs an empty path, this could start at the root of the file system, and try to scan every file.  Consider checking for such things and maybe using os.path.normpath or path.resolve() in pathlib.  pathlib also provides exists() and glob().

find_todos()

if exists(path) should probably come before it is used in the call to _get_files():

if exists(path):
    files = _get_files(path, ext,recursive=recursive)
    ...


A PermissionError might occur when a filed is opened, so I would tend to put the try:...except PermissionError: block in _find_todos_in_file()

_find_todos_in_file()

Each line is scanned at least twice, and maybe four times:

if comment_start in line and todo_token in line: # two scans
        ....
    comment_index = line.find(comment_start)     # one scan
    todo_index = line.find(todo_token)           # one scan


line.find(comment_start) returns -1 if the comment_start isn't in the line.  Also, find() takes a parameter that says where to start searching.  So the above code can be simplified and made more efficient like so:

comment_index = line.find(comment_start)
if comment_index &gt;= 0:
    todo_index = line.find(todo_token, comment_index)

    if todo_index &gt;= 0:
        temp_todos.append((line_no+1, line.strip))


I changed what gets appended.  The file name is redundant, it gets passed in as an argument, so the caller already knows what it is -- find_todos() uses it as a dict key.  And I put line_no and the text in a tuple rather than format them into a string.  Putting them in a string seems more like a job for an output function.  And keeping them as a tuple is more more flexible (e.g. you want to put them in a database, or see if TODOs show up more near the end of a file, etc).

other stuff

find_todos() and find_todos_in_file() basically iterate over all the lines in a bunch of files.  This is what the fileinput library does.  These two function could be combined and rewritten something like this:

def find_todos(path, ext, todo_token, comment_start, recursive):
    todos = defaultdict(list)

    files = glob("{path}/**/*{ext}", recursive=recursive)

    with fileinput.input(files=files) as f:
        for line in f:
            if f.isfirstline():
                filename = f.filename()
                print(f"Searching: {filename}")

            comment_index = line.find(comment_start)
            if comment_index &gt;= 0:
                todo_index = line.find(todo_token, comment_index)

                if todo_index &gt;= 0:
                    todos[filename].append((f.filelineno(), line.strip))


For a simple command line tool, the argparse library can help with parsing command parameters and options.  For more complicated or enhanced tools try a command line tool library like Click or Python Prompt.

def make_arg_parser():
    import argparse

    parser = argparse.ArgumentParser(
                        description='TODO finder/lister.')

    parser.add_argument('path',
                        default='.',
                        help="where to search for TODO's")
    parser.add_argument('ext',
                        default='py',
                        help="kind of files to search")
    parser.add_argument('comment',
                        default='#',
                        help="line comment marker")
    parser.add_argument('todo',
                        default='TODO',
                        help="text to mark TODO items")
    parser.add_argument('--norecurse',
                        action='store_false',
                        dest='recursive',
                        help="don't recurse into subdirs")
    parser.add_argument('--csv',
                        action='store_true',
                        help="save to a csv file")

    return parser

if __name__ == "__main__":
    cli_parser = make_arg_parser()
    args = cli_parser.parse_args()

    todos = find_todos(args.path,
                       ext=args.ext,
                       todo_token=args.todo,
                       comment_start=args.comment,
                       recursive=args.recursive)
show_todos(todos)

if args.csv:
    save_csv(todos, 'py')


The above is just an example.  It mirrors the call to find_todos() and adds an argument for todo_token and a flag to save to a csv file.