Question: Given an ordered list of words with associated info (or a list of tuples). I want to replace some of the strings with others but keep track of the associated info. Let's say that we have a simple case where our input data is two list: words = ["hello", "I", "am", "I", "am", "Jone", "101"] info = ["1", "3", "23", "4", "6", "5", "12"]  input could also be just a list of tuples: list_tuples = list(zip(words, info)))  Each item of "list_words" has an associated item (with the same index) from "list_info". e.g. "hello" corresponds to "1" and the second "I" corresponds to "4". I want to apply some normalization rules to transform them into: words = ["hello", "I'm", "I'm", "Jone", "one hundred and one"] info = ["1", ["3", "23"], ["4", "6"], "5", "12"]  or to another possible solution: words = ["hello", "I'm", "I'm", "Jone", "one", "hundred", "and", "one"] info = ["1", ["3", "23"], ["4", "6"], "5", "12", "12", "12", "12"]  Note this is a simple case, and the idea is to apply multiple normalization rules (numbers to words, substitutions, other contractions, etc.). I know how to transform my string into another using regex, but in that case, I am losing the associated information: def normalize_texts_loosing_info(text): # Normalization rules text = re.sub(r"I am", "I\'m", text) text = re.sub(r"101", "one hundred and one", text) # other normalization rules. e.g. # text = re.sub(r"we\'ll", "we will", text) # text = re.sub(r"you are", "you\'re", text) # .... return text.split() words = ["hello", "I", "am", "I", "am", "Jone", "101"] print(words) print(" ".join(words)) output = normalize_texts(" ".join(words)) print(output)  Question is: How can I apply some transformations to an ordered string/list of words but keep the associated info of those words? PD: Thank you for all the useful comments

Answer: Using partial matching capability of the the regex library, one can keep track of which patterns might still be applicable. def apply_transformations(words_infos: list[tuple[str, Any]], transformations: dict[regex.Pattern, tuple[str, ...]]) \ -> list[tuple[str, Any]]: # Create the list which we modify to create the result result = words_infos.copy() # Keep track of the difference between the lengths of input and output offset = 0 # Apply a transformation of tokens [start, end) replacing them with the tokens in new # During that we collect together the infos of all input tokens into one tuple, # then we copy this info to all new tokens we create # Afterwards we update `offset` def apply(start: int, end: int, new: tuple[str, ...]): nonlocal offset print(start, end, new, offset) new_info = tuple(words_infos[i][1] for i in range(start, end)) if len(new_info) == 1: new_info, = new_info result[start + offset: end + offset] = [(w, new_info) for w in new] offset -= (end - start) - len(new) # Keep track of partial matches that might still be applied partials = [] for i, (word, info) in enumerate(words_infos): new_partials = [] # Try all patterns starting at this token for pattern, res in transformations.items(): if m := pattern.fullmatch(word, partial=True): if m.partial: # We have a partial match, add it to the backlog new_partials.append((pattern, i, word)) else: # Apply this transformation immediately, replacing only this token. apply(i, i + 1, res) partials = [] # After applying something, we completely aboard everything else going on new_partials = [] break # Look thought the backlog, add the current token to them and check if the pattern is now fully applied. for pattern, first, prefix in partials: if m := pattern.fullmatch(prefix + " " + word, partial=True): if m.partial: new_partials.append((pattern, first, prefix + " " + word)) else: apply(first, i + 1, transformations[pattern]) new_partials = [] break partials = new_partials return result  This function makes use of the more sane list[tuple] representation of the data instead of keep two lists in sync. The transformations are defined as a mapping from regex.Pattern instances (i..e not regexes themself) to sequences of strings. If you want more complex transformations, i.e. more similar to the input of re.sub you can fiddle around with that by replacing the values in the transformations dict and modifying the local apply function. def main(): words = ["hello", "I", "am", "I", "am", "Jone", "101"] info = ["1", "3", "23", "4", "6", "5", "12"] word_infos = list(zip(words, info, strict=True)) transformations = { regex.compile(r"I am"): ("I\'m",), regex.compile(r"101"): ("one hundred and one",) } result = apply_transformations(word_infos, transformations) print(result)