If you only want adjacent values, it's probably most efficient to slide a window across the input, slicing for the substrings you want, rather than splitting and joining them
As a bonus, the same logic will work for lists
def multi_adjacent(letters, min_gap=2):
# further opportunity for dynamic maximum length and bounds-checking
for idx_start in range(len(letters)-(min_gap-1)):
for idx_end in range(idx_start+min_gap, len(letters)+1):
yield letters[idx_start:idx_end] # slice for substring
>>> list(multi_adjacent("abcd"))
['ab', 'abc', 'abcd', 'bc', 'bcd', 'cd']
>>> list(multi_adjacent("abcd", 3))
['abc', 'abcd', 'bcd']
>>> list(multi_adjacent([1,2,3,4]))
[[1, 2], [1, 2, 3], [1, 2, 3, 4], [2, 3], [2, 3, 4], [3, 4]]
If you're really after every grouping (not just adjacent ones), itertools.combinations
can provide this
from itertools import combinations
def multi_combination(letters):
for grouping_size in range(2, len(letters)+1):
for grouping in combinations(letters, grouping_size):
yield "".join(grouping)
>>> sorted(multi_combination("abcd"))
['ab', 'abc', 'abcd', 'abd', 'ac', 'acd', 'ad', 'bc', 'bcd', 'bd', 'cd']