Repository: huggingface/datasets
Pull request: f-string formatting
**Fix #3257**

Replaced _.format()_ and _%_ by f-strings in the following modules : 
- [x] **tests**
- [x] **metrics**
- [x] **benchmarks**
- [x] **utils**
- [x] **templates**
- [x] **src/Datasets/\*.py**

Modules in **_src/Datasets/_**: 
- [x] **commands**
- [x] **features**
- [x] **formatting**
- [x] **io**
- [x] **tasks**
- [x] **utils**


Module **datasets** will not be edited as asked by @mariosasko 

-A correction of the first PR (#3267)-


Problem statement:
Use f-strings for string formatting 
f-strings offer better readability/performance than `str.format` and `%`, so we should use them in all places in our codebase unless there is good reason to keep the older syntax.

> **NOTE FOR CONTRIBUTORS**: To avoid large PRs and possible merge conflicts, do 1-3 modules per PR. Also, feel free to ignore the files located under `datasets/*`.

Hints from issue discussion:
Hi, I would be glad to help with this. Is there anyone else working on it?
Hi, I would be glad to work on this too.
#self-assign

Commit message: make style on edited files

Patch to review:
diff --git a/benchmarks/format.py b/benchmarks/format.py
index b834b74c414..7ab2a564ff2 100644
--- a/benchmarks/format.py
+++ b/benchmarks/format.py
@@ -24,12 +24,12 @@ def format_json_to_md(input_json_file, output_md_file):
             old_val = metric_vals.get("old", None)
             dif_val = metric_vals.get("diff", None)
 
-            val_str = " {:f}".format(new_val) if isinstance(new_val, (int, float)) else "None"
+            val_str = f" {new_val:f}" if isinstance(new_val, (int, float)) else "None"
 
             if old_val is not None:
-                val_str += " / {:f}".format(old_val) if isinstance(old_val, (int, float)) else "None"
+                val_str += f" / {new_val:f}" if isinstance(old_val, (int, float)) else "None"
             if dif_val is not None:
-                val_str += " ({:f})".format(dif_val) if isinstance(dif_val, (int, float)) else "None"
+                val_str += f" ({dif_val:f})" if isinstance(dif_val, (int, float)) else "None"
 
             title += " " + metric_name + " |"
             lines += "---|"
diff --git a/metrics/coval/coval.py b/metrics/coval/coval.py
index 20d22b6b010..4771ad99485 100644
--- a/metrics/coval/coval.py
+++ b/metrics/coval/coval.py
@@ -210,17 +210,17 @@ def get_coref_infos(
     if remove_nested:
         logger.info(
             "Number of removed nested coreferring mentions in the key "
-            "annotation: %s; and system annotation: %s" % (key_nested_coref_num, sys_nested_coref_num)
+            f"annotation: {key_nested_coref_num}; and system annotation: {sys_nested_coref_num}"
         )
         logger.info(
             "Number of resulting singleton clusters in the key "
-            "annotation: %s; and system annotation: %s" % (key_removed_nested_clusters, sys_removed_nested_clusters)
+            f"annotation: {key_removed_nested_clusters}; and system annotation: {sys_removed_nested_clusters}"
         )
 
     if not keep_singletons:
         logger.info(
-            "%d and %d singletons are removed from the key and system "
-            "files, respectively" % (key_singletons_num, sys_singletons_num)
+            f"{key_singletons_num:d} and {sys_singletons_num:d} singletons are removed from the key and system "
+            "files, respectively"
         )
 
     return doc_coref_infos
@@ -242,14 +242,14 @@ def evaluate(key_lines, sys_lines, metrics, NP_only, remove_nested, keep_singlet
 
         logger.info(
             name.ljust(10),
-            "Recall: %.2f" % (recall * 100),
-            " Precision: %.2f" % (precision * 100),
-            " F1: %.2f" % (f1 * 100),
+            f"Recall: {recall * 100:.2f}",
+            f" Precision: {precision * 100:.2f}",
+            f" F1: {f1 * 100:.2f}",
         )
 
     if conll_subparts_num == 3:
         conll = (conll / 3) * 100
-        logger.info("CoNLL score: %.2f" % conll)
+        logger.info(f"CoNLL score: {conll:.2f}")
         output_scores.update({"conll_score": conll})
 
     return output_scores
diff --git a/metrics/squad_v2/evaluate.py b/metrics/squad_v2/evaluate.py
index 549688a7358..965c7e8ed2c 100644
--- a/metrics/squad_v2/evaluate.py
+++ b/metrics/squad_v2/evaluate.py
@@ -113,7 +113,7 @@ def get_raw_scores(dataset, preds):
                     # For unanswerable questions, only correct answer is empty string
                     gold_answers = [""]
                 if qid not in preds:
-                    print("Missing prediction for %s" % qid)
+                    print(f"Missing prediction for {qid}")
                     continue
                 a_pred = preds[qid]
                 # Take max over all gold answers
@@ -156,7 +156,7 @@ def make_eval_dict(exact_scores, f1_scores, qid_list=None):
 
 def merge_eval(main_eval, new_eval, prefix):
     for k in new_eval:
-        main_eval["%s_%s" % (prefix, k)] = new_eval[k]
+        main_eval[f"{prefix}_{k}"] = new_eval[k]
 
 
 def plot_pr_curve(precisions, recalls, out_image, title):
@@ -238,8 +238,8 @@ def histogram_na_prob(na_probs, qid_list, image_dir, name):
     plt.hist(x, weights=weights, bins=20, range=(0.0, 1.0))
     plt.xlabel("Model probability of no-answer")
     plt.ylabel("Proportion of dataset")
-    plt.title("Histogram of no-answer probability: %s" % name)
-    plt.savefig(os.path.join(image_dir, "na_prob_hist_%s.png" % name))
+    plt.title(f"Histogram of no-answer probability: {name}")
+    plt.savefig(os.path.join(image_dir, f"na_prob_hist_{name}.png"))
     plt.clf()
 
 
diff --git a/metrics/super_glue/record_evaluation.py b/metrics/super_glue/record_evaluation.py
index bfbcfe6f025..396d7ac8abc 100644
--- a/metrics/super_glue/record_evaluation.py
+++ b/metrics/super_glue/record_evaluation.py
@@ -63,7 +63,7 @@ def evaluate(dataset, predictions):
         for qa in passage["qas"]:
             total += 1
             if qa["id"] not in predictions:
-                message = "Unanswered question {} will receive score 0.".format(qa["id"])
+                message = f'Unanswered question {qa["id"]} will receive score 0.'
                 print(message, file=sys.stderr)
                 continue
 
@@ -95,7 +95,7 @@ def evaluate(dataset, predictions):
         dataset_json = json.load(data_file)
         if dataset_json["version"] != expected_version:
             print(
-                "Evaluation expects v-{}, but got dataset with v-{}".format(expected_version, dataset_json["version"]),
+                f'Evaluation expects v-{expected_version}, but got dataset with v-{dataset_json["version"]}',
                 file=sys.stderr,
             )
         dataset = dataset_json["data"]
@@ -106,6 +106,6 @@ def evaluate(dataset, predictions):
     metrics, correct_ids = evaluate(dataset, predictions)
 
     if args.output_correct_ids:
-        print("Output {} correctly answered question IDs.".format(len(correct_ids)))
+        print(f"Output {len(correct_ids)} correctly answered question IDs.")
         with open("correct_ids.json", "w") as f:
             json.dump(correct_ids, f)
diff --git a/metrics/super_glue/super_glue.py b/metrics/super_glue/super_glue.py
index d0ef59257ff..588e305cc92 100644
--- a/metrics/super_glue/super_glue.py
+++ b/metrics/super_glue/super_glue.py
@@ -125,7 +125,7 @@ def evaluate_multirc(ids_preds, labels):
     """
     question_map = {}
     for id_pred, label in zip(ids_preds, labels):
-        question_id = "{}-{}".format(id_pred["idx"]["paragraph"], id_pred["idx"]["question"])
+        question_id = f'{id_pred["idx"]["paragraph"]}-{id_pred["idx"]["question"]}'
         pred = id_pred["prediction"]
         if question_id in question_map:
             question_map[question_id].append((pred, label))
diff --git a/src/datasets/arrow_dataset.py b/src/datasets/arrow_dataset.py
index 2b5951d6bce..be2b43bffb6 100644
--- a/src/datasets/arrow_dataset.py
+++ b/src/datasets/arrow_dataset.py
@@ -621,9 +621,7 @@ def __init__(
         assert self._fingerprint is not None, "Fingerprint can't be None in a Dataset object"
         if self.info.features.type != inferred_features.type:
             raise ValueError(
-                "External features info don't match the dataset:\nGot\n{}\nwith type\n{}\n\nbut expected something like\n{}\nwith type\n{}".format(
-                    self.info.features, self.info.features.type, inferred_features, inferred_features.type
-                )
+                f"External features info don't match the dataset:\nGot\n{self.info.features}\nwith type\n{self.info.features.type}\n\nbut expected something like\n{inferred_features}\nwith type\n{inferred_features.type}"
             )
 
         if self._indices is not None:
@@ -735,9 +733,7 @@ def from_pandas(
         """
         if info is not None and features is not None and info.features != features:
             raise ValueError(
-                "Features specified in `features` and `info.features` can't be different:\n{}\n{}".format(
-                    features, info.features
-                )
+                f"Features specified in `features` and `info.features` can't be different:\n{features}\n{info.features}"
             )
         features = features if features is not None else info.features if info is not None else None
         if info is None:
@@ -768,9 +764,7 @@ def from_dict(
         """
         if info is not None and features is not None and info.features != features:
             raise ValueError(
-                "Features specified in `features` and `info.features` can't be different:\n{}\n{}".format(
-                    features, info.features
-                )
+                f"Features specified in `features` and `info.features` can't be different:\n{features}\n{info.features}"
             )
         features = features if features is not None else info.features if info is not None else None
         if info is None:
@@ -1008,7 +1002,7 @@ def save_to_disk(self, dataset_path: str, fs=None):
             # Sort only the first level of keys, or we might shuffle fields of nested features if we use sort_keys=True
             sorted_keys_dataset_info = {key: dataset_info[key] for key in sorted(dataset_info)}
             json.dump(sorted_keys_dataset_info, dataset_info_file, indent=2)
-        logger.info("Dataset saved in {}".format(dataset_path))
+        logger.info(f"Dataset saved in {dataset_path}")
 
     @staticmethod
     def load_from_disk(dataset_path: str, fs=None, keep_in_memory: Optional[bool] = None) -> "Dataset":
@@ -1216,9 +1210,7 @@ def flatten_(self, max_depth=16):
                 break
         self.info.features = Features.from_arrow_schema(self._data.schema)
         self._data = update_metadata_with_features(self._data, self.features)
-        logger.info(
-            "Flattened dataset from depth {} to depth {}.".format(depth, 1 if depth + 1 < max_depth else "unknown")
-        )
+        logger.info(f'Flattened dataset from depth {depth} to depth { 1 if depth + 1 < max_depth else "unknown"}.')
 
     @fingerprint_transform(inplace=False)
     def flatten(self, new_fingerprint, max_depth=16) -> "Dataset":
@@ -1237,9 +1229,7 @@ def flatten(self, new_fingerprint, max_depth=16) -> "Dataset":
                 break
         dataset.info.features = Features.from_arrow_schema(dataset._data.schema)
         dataset._data = update_metadata_with_features(dataset._data, dataset.features)
-        logger.info(
-            "Flattened dataset from depth {} to depth {}.".format(depth, 1 if depth + 1 < max_depth else "unknown")
-        )
+        logger.info(f'Flattened dataset from depth {depth} to depth {1 if depth + 1 < max_depth else "unknown"}.')
         dataset._fingerprint = new_fingerprint
         return dataset
 
@@ -1688,9 +1678,7 @@ def set_format(
             columns = list(columns)
         if columns is not None and any(col not in self._data.column_names for col in columns):
             raise ValueError(
-                "Columns {} not in the dataset. Current columns in the dataset: {}".format(
-                    list(filter(lambda col: col not in self._data.column_names, columns)), self._data.column_names
-                )
+                f"Columns {list(filter(lambda col: col not in self._data.column_names, columns))} not in the dataset. Current columns in the dataset: {self._data.column_names}"
             )
         if columns is not None:
             columns = columns.copy()  # Ensures modifications made to the list after this call don't cause bugs
@@ -2002,9 +1990,7 @@ def decorated(item, *args, **kwargs):
             for input_column in input_columns:
                 if input_column not in self._data.column_names:
                     raise ValueError(
-                        "Input column {} not in the dataset. Current columns in the dataset: {}".format(
-                            input_column, self._data.column_names
-                        )
+                        f"Input column {input_column} not in the dataset. Current columns in the dataset: {self._data.column_names}"
                     )
 
         if isinstance(remove_columns, str):
@@ -2012,10 +1998,7 @@ def decorated(item, *args, **kwargs):
 
         if remove_columns is not None and any(col not in self._data.column_names for col in remove_columns):
             raise ValueError(
-                "Column to remove {} not in the dataset. Current columns in the dataset: {}".format(
-                    list(filter(lambda col: col not in self._data.column_names, remove_columns)),
-                    self._data.column_names,
-                )
+                f"Column to remove {list(filter(lambda col: col not in self._data.column_names, remove_columns))} not in the dataset. Current columns in the dataset: {self._data.column_names}"
             )
 
         load_from_cache_file = load_from_cache_file if load_from_cache_file is not None else is_caching_enabled()
@@ -2057,7 +2040,7 @@ def format_cache_file_name(cache_file_name, rank):
                 sep = cache_file_name.rindex(".")
                 base_name, extension = cache_file_name[:sep], cache_file_name[sep:]
                 cache_file_name = base_name + suffix_template.format(rank=rank, num_proc=num_proc) + extension
-                logger.info("Process #{} will write at {}".format(rank, cache_file_name))
+                logger.info(f"Process #{rank} will write at {cache_file_name}")
                 return cache_file_name
 
             prev_env = deepcopy(os.environ)
@@ -2126,7 +2109,7 @@ def catch_non_existent_error(func, kwargs):
             if nb_of_missing_shards > 0:
                 with Pool(nb_of_missing_shards, initargs=initargs, initializer=initializer) as pool:
                     os.environ = prev_env
-                    logger.info("Spawning {} processes".format(num_proc))
+                    logger.info(f"Spawning {num_proc} processes")
                     results = {
                         i: pool.apply_async(self.__class__._map_single, kwds=kwds)
                         for i, (kwds, cached_shard) in enumerate(zip(kwds_per_shard, transformed_shards))
@@ -2143,7 +2126,7 @@ def catch_non_existent_error(func, kwargs):
                 transformed_shards.count(None) == 0
             ), "All shards have to be defined Datasets, none should still be missing."
 
-            logger.info("Concatenating {} shards".format(num_proc))
+            logger.info(f"Concatenating {num_proc} shards")
             result = concatenate_datasets(transformed_shards)
             if new_fingerprint is not None:
                 result._fingerprint = new_fingerprint
@@ -2240,7 +2223,7 @@ def _map_single(
                 # current dataset file and the mapping args
                 cache_file_name = self._get_cache_file_path(new_fingerprint)
             if os.path.exists(cache_file_name) and load_from_cache_file:
-                logger.warning("Loading cached processed dataset at %s", cache_file_name)
+                logger.warning(f"Loading cached processed dataset at {cache_file_name}")
                 info = self.info.copy()
                 info.features = features
                 info.task_templates = None
@@ -2262,9 +2245,7 @@ def validate_function_output(processed_inputs, indices):
             """Validate output of the map function."""
             if processed_inputs is not None and not isinstance(processed_inputs, (Mapping, pa.Table)):
                 raise TypeError(
-                    "Provided `function` which is applied to all elements of table returns a variable of type {}. Make sure provided `function` returns a variable of type `dict` (or a pyarrow table) to update the dataset or `None` if you are only interested in side effects.".format(
-                        type(processed_inputs)
-                    )
+                    f"Provided `function` which is applied to all elements of table returns a variable of type {type(processed_inputs)}. Make sure provided `function` returns a variable of type `dict` (or a pyarrow table) to update the dataset or `None` if you are only interested in side effects."
                 )
             elif isinstance(indices, list) and isinstance(processed_inputs, Mapping):
                 allowed_batch_return_types = (list, np.ndarray)
@@ -2273,9 +2254,7 @@ def validate_function_output(processed_inputs, indices):
                 )
                 if all_dict_values_are_lists is False:
                     raise TypeError(
-                        "Provided `function` which is applied to all elements of table returns a `dict` of types {}. When using `batched=True`, make sure provided `function` returns a `dict` of types like `{}`.".format(
-                            [type(x) for x in processed_inputs.values()], allowed_batch_return_types
-                        )
+                        "Provided `function` which is applied to all elements of table returns a `dict` of types {[type(x) for x in processed_inputs.values()]}. When using `batched=True`, make sure provided `function` returns a `dict` of types like `{allowed_batch_return_types}`."
                     )
 
         def apply_function_on_filtered_inputs(inputs, indices, check_same_num_examples=False, offset=0):
@@ -2338,7 +2317,7 @@ def init_buffer_and_writer():
                 )
             else:
                 buf_writer = None
-                logger.info("Caching processed dataset at %s", cache_file_name)
+                logger.info(f"Caching processed dataset at {cache_file_name}")
                 tmp_file = tempfile.NamedTemporaryFile("wb", dir=os.path.dirname(cache_file_name), delete=False)
                 writer = ArrowWriter(
                     features=writer_features,
@@ -2653,7 +2632,7 @@ def select(
             )
         else:
             buf_writer = None
-            logger.info("Caching indices mapping at %s", indices_cache_file_name)
+            logger.info(f"Caching indices mapping at {indices_cache_file_name}")
             tmp_file = tempfile.NamedTemporaryFile("wb", dir=os.path.dirname(indices_cache_file_name), delete=False)
             writer = ArrowWriter(
                 path=tmp_file.name, writer_batch_size=writer_batch_size, fingerprint=new_fingerprint, unit="indices"
@@ -2738,10 +2717,7 @@ def sort(
         # Check the column name
         if not isinstance(column, str) or column not in self._data.column_names:
             raise ValueError(
-                "Column '{}' not found in the dataset. Please provide a column selected in: {}".format(
-                    column,
-                    self._data.column_names,
-                )
+                f"Column '{column}' not found in the dataset. Please provide a column selected in: {self._data.column_names}"
             )
 
         # Check if we've already cached this computation (indexed by a hash)
@@ -2750,7 +2726,7 @@ def sort(
                 # we create a unique hash from the function, current dataset file and the mapping args
                 indices_cache_file_name = self._get_cache_file_path(new_fingerprint)
             if os.path.exists(indices_cache_file_name) and load_from_cache_file:
-                logger.warning("Loading cached sorted indices for dataset at %s", indices_cache_file_name)
+                logger.warning(f"Loading cached sorted indices for dataset at {indices_cache_file_name}")
                 return self._new_dataset_with_indices(
                     fingerprint=new_fingerprint, indices_cache_file_name=indices_cache_file_name
                 )
@@ -2833,7 +2809,7 @@ def shuffle(
                 # we create a unique hash from the function, current dataset file and the mapping args
                 indices_cache_file_name = self._get_cache_file_path(new_fingerprint)
             if os.path.exists(indices_cache_file_name) and load_from_cache_file:
-                logger.warning("Loading cached shuffled indices for dataset at %s", indices_cache_file_name)
+                logger.warning(f"Loading cached shuffled indices for dataset at {indices_cache_file_name}")
                 return self._new_dataset_with_indices(
                     fingerprint=new_fingerprint, indices_cache_file_name=indices_cache_file_name
                 )
@@ -3008,9 +2984,7 @@ def train_test_split(
                 and load_from_cache_file
             ):
                 logger.warning(
-                    "Loading cached split indices for dataset at %s and %s",
-                    train_indices_cache_file_name,
-                    test_indices_cache_file_name,
+                    f"Loading cached split indices for dataset at {train_indices_cache_file_name} and {test_indices_cache_file_name}"
                 )
                 return DatasetDict(
                     {
diff --git a/src/datasets/arrow_reader.py b/src/datasets/arrow_reader.py
index 263152fbcf3..2a834a48b8f 100644
--- a/src/datasets/arrow_reader.py
+++ b/src/datasets/arrow_reader.py
@@ -44,9 +44,9 @@
 HF_GCP_BASE_URL = "https://storage.googleapis.com/huggingface-nlp/cache/datasets"
 
 _SUB_SPEC_RE = re.compile(
-    r"""
+    fr"""
 ^
- (?P<split>{split_re})
+ (?P<split>{_split_re[1:-1]})
  (\[
     ((?P<from>-?\d+)
      (?P<from_pct>%)?)?
@@ -55,9 +55,7 @@
      (?P<to_pct>%)?)?
  \])?(\((?P<rounding>[^\)]*)\))?
 $
-""".format(
-        split_re=_split_re[1:-1]
-    ),  # remove ^ and $
+""",  # remove ^ and $
     re.X,
 )
 
@@ -212,7 +210,7 @@ def read(
 
         files = self.get_file_instructions(name, instructions, split_infos)
         if not files:
-            msg = 'Instruction "%s" corresponds to no data!' % instructions
+            msg = f'Instruction "{instructions}" corresponds to no data!'
             raise AssertionError(msg)
         return self.read_files(files=files, original_instructions=instructions, in_memory=in_memory)
 
@@ -394,7 +392,7 @@ def _str_to_read_instruction(spec):
     """Returns ReadInstruction for given string."""
     res = _SUB_SPEC_RE.match(spec)
     if not res:
-        raise AssertionError("Unrecognized instruction format: %s" % spec)
+        raise AssertionError(f"Unrecognized instruction format: {spec}")
     unit = "%" if res.group("from_pct") or res.group("to_pct") else "abs"
     return ReadInstruction(
         split_name=res.group("split"),
@@ -430,7 +428,7 @@ def _rel_to_abs_instr(rel_instr, name2len):
     pct_to_abs = _pct_to_abs_closest if rel_instr.rounding == "closest" else _pct_to_abs_pct1
     split = rel_instr.splitname
     if split not in name2len:
-        raise ValueError('Unknown split "{}". Should be one of {}.'.format(split, list(name2len)))
+        raise ValueError(f'Unknown split "{split}". Should be one of {list(name2len)}.')
     num_examples = name2len[split]
     from_ = rel_instr.from_
     to = rel_instr.to
@@ -441,7 +439,7 @@ def _rel_to_abs_instr(rel_instr, name2len):
         from_ = 0 if from_ is None else from_
         to = num_examples if to is None else to
     if abs(from_) > num_examples or abs(to) > num_examples:
-        msg = "Requested slice [%s:%s] incompatible with %s examples." % (from_ or "", to or "", num_examples)
+        msg = f'Requested slice [{from_ or ""}:{to or ""}] incompatible with {num_examples} examples.'
         raise AssertionError(msg)
     if from_ < 0:
         from_ = num_examples + from_
@@ -559,7 +557,7 @@ def from_spec(cls, spec):
         spec = str(spec)  # Need to convert to str in case of NamedSplit instance.
         subs = _ADDITION_SEP_RE.split(spec)
         if not subs:
-            raise AssertionError("No instructions could be built out of %s" % spec)
+            raise AssertionError(f"No instructions could be built out of {spec}")
         instruction = _str_to_read_instruction(subs[0])
         return sum([_str_to_read_instruction(sub) for sub in subs[1:]], instruction)
 
@@ -602,7 +600,7 @@ def __str__(self):
         return self.to_spec()
 
     def __repr__(self):
-        return "ReadInstruction(%s)" % self._relative_instructions
+        return f"ReadInstruction({self._relative_instructions})"
 
     def to_absolute(self, name2len):
         """Translate instruction into a list of absolute instructions.
diff --git a/src/datasets/arrow_writer.py b/src/datasets/arrow_writer.py
index 4d2783963cc..c8fc28581af 100644
--- a/src/datasets/arrow_writer.py
+++ b/src/datasets/arrow_writer.py
@@ -152,9 +152,7 @@ def __arrow_array__(self, type=None):
                 except pa.lib.ArrowInvalid as e:
                     if "overflow" in str(e):
                         raise OverflowError(
-                            "There was an overflow with type {}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({})".format(
-                                type_(self.data), e
-                            )
+                            f"There was an overflow with type {type_(self.data)}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({e})"
                         ) from None
                     elif trying_int_optimization and "not in range" in str(e):
                         optimized_int_type_str = np.dtype(self.optimized_int_type.to_pandas_dtype()).name
@@ -164,9 +162,7 @@ def __arrow_array__(self, type=None):
                         raise
             elif "overflow" in str(e):
                 raise OverflowError(
-                    "There was an overflow with type {}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({})".format(
-                        type_(self.data), e
-                    )
+                    f"There was an overflow with type {type_(self.data)}. Try to reduce writer_batch_size to have batches smaller than 2GB.\n({e})"
                 ) from None
             elif trying_int_optimization and "not in range" in str(e):
                 optimized_int_type_str = np.dtype(self.optimized_int_type.to_pandas_dtype()).name
@@ -339,9 +335,7 @@ def write_examples_on_file(self):
                 # This check fails with FloatArrays with nans, which is not what we want, so account for that:
                 if not isinstance(pa_array[0], pa.lib.FloatScalar):
                     raise OverflowError(
-                        "There was an overflow in the {}. Try to reduce writer_batch_size to have batches smaller than 2GB".format(
-                            type(pa_array)
-                        )
+                        f"There was an overflow in the {type(pa_array)}. Try to reduce writer_batch_size to have batches smaller than 2GB"
                     )
             arrays.append(pa_array)
             inferred_types.append(inferred_type)
@@ -473,11 +467,7 @@ def finalize(self, close_stream=True):
         if close_stream:
             self.stream.close()
         logger.debug(
-            "Done writing %s %s in %s bytes %s.",
-            self._num_examples,
-            self.unit,
-            self._num_bytes,
-            self._path if self._path else "",
+            f"Done writing {self._num_examples} {self.unit} in {self._num_bytes} bytes {self._path if self._path else ''}."
         )
         return self._num_examples, self._num_bytes
 
@@ -550,7 +540,7 @@ def finalize(self, metrics_query_result: dict):
         from .utils import beam_utils
 
         # Convert to arrow
-        logger.info("Converting parquet file {} to arrow {}".format(self._parquet_path, self._path))
+        logger.info(f"Converting parquet file {self._parquet_path} to arrow {self._path}")
         shards = [
             metadata.path
             for metadata in beam.io.filesystems.FileSystems.match([self._parquet_path + "*.parquet"])[0].metadata_list
diff --git a/src/datasets/builder.py b/src/datasets/builder.py
index b4c19403072..81735dd0afd 100644
--- a/src/datasets/builder.py
+++ b/src/datasets/builder.py
@@ -289,9 +289,7 @@ def __init__(
                     self.info = DatasetInfo.from_directory(self._cache_dir)
                 else:  # dir exists but no data, remove the empty dir as data aren't available anymore
                     logger.warning(
-                        "Old caching folder {} for dataset {} exists but not data were found. Removing it. ".format(
-                            self._cache_dir, self.name
-                        )
+                        f"Old caching folder {self._cache_dir} for dataset {self.name} exists but not data were found. Removing it. "
                     )
                     os.rmdir(self._cache_dir)
 
@@ -331,25 +329,23 @@ def _create_builder_config(self, name=None, custom_features=None, **config_kwarg
         if name is None and self.BUILDER_CONFIGS and not config_kwargs:
             if self.DEFAULT_CONFIG_NAME is not None:
                 builder_config = self.builder_configs.get(self.DEFAULT_CONFIG_NAME)
-                logger.warning("No config specified, defaulting to: %s/%s", self.name, builder_config.name)
+                logger.warning(f"No config specified, defaulting to: {self.name}/{builder_config.name}")
             else:
                 if len(self.BUILDER_CONFIGS) > 1:
-                    example_of_usage = "load_dataset('{}', '{}')".format(self.name, self.BUILDER_CONFIGS[0].name)
+                    example_of_usage = f"load_dataset('{self.name}', '{self.BUILDER_CONFIGS[0].name}')"
                     raise ValueError(
                         "Config name is missing."
-                        "\nPlease pick one among the available configs: %s" % list(self.builder_configs.keys())
-                        + "\nExample of usage:\n\t`{}`".format(example_of_usage)
+                        f"\nPlease pick one among the available configs: {list(self.builder_configs.keys())}"
+                        + f"\nExample of usage:\n\t`{example_of_usage}`"
                     )
                 builder_config = self.BUILDER_CONFIGS[0]
-                logger.info("No config specified, defaulting to first: %s/%s", self.name, builder_config.name)
+                logger.info(f"No config specified, defaulting to first: {self.name}/{builder_config.name}")
 
         # try get config by name
         if isinstance(name, str):
             builder_config = self.builder_configs.get(name)
             if builder_config is None and self.BUILDER_CONFIGS:
-                raise ValueError(
-                    "BuilderConfig %s not found. Available: %s" % (name, list(self.builder_configs.keys()))
-                )
+                raise ValueError(f"BuilderConfig {name} not found. Available: {list(self.builder_configs.keys())}")
 
         # if not using an existing config, then create a new config on the fly with config_kwargs
         if not builder_config:
@@ -369,7 +365,7 @@ def _create_builder_config(self, name=None, custom_features=None, **config_kwarg
                     setattr(builder_config, key, value)
 
         if not builder_config.name:
-            raise ValueError("BuilderConfig must have a name, got %s" % builder_config.name)
+            raise ValueError(f"BuilderConfig must have a name, got {builder_config.name}")
 
         # compute the config id that is going to be used for caching
         config_id = builder_config.create_config_id(
@@ -378,18 +374,17 @@ def _create_builder_config(self, name=None, custom_features=None, **config_kwarg
         )
         is_custom = config_id not in self.builder_configs
         if is_custom:
-            logger.warning("Using custom data configuration %s", config_id)
+            logger.warning(f"Using custom data configuration {config_id}")
         else:
             if builder_config != self.builder_configs[builder_config.name]:
                 raise ValueError(
                     "Cannot name a custom BuilderConfig the same as an available "
-                    "BuilderConfig. Change the name. Available BuilderConfigs: %s"
-                    % (list(self.builder_configs.keys()))
+                    f"BuilderConfig. Change the name. Available BuilderConfigs: {list(self.builder_configs.keys())}"
                 )
             if not builder_config.version:
-                raise ValueError("BuilderConfig %s must have a version" % builder_config.name)
+                raise ValueError(f"BuilderConfig {builder_config.name} must have a version")
             # if not builder_config.description:
-            #     raise ValueError("BuilderConfig %s must have a description" % builder_config.name)
+            #     raise ValueError(f"BuilderConfig {builder_config.name} must have a description"  )
 
         return builder_config, config_id
 
@@ -401,7 +396,7 @@ def builder_configs(cls):
         configs = {config.name: config for config in cls.BUILDER_CONFIGS}
         if len(configs) != len(cls.BUILDER_CONFIGS):
             names = [config.name for config in cls.BUILDER_CONFIGS]
-            raise ValueError("Names in BUILDER_CONFIGS must not be duplicated. Got %s" % names)
+            raise ValueError(f"Names in BUILDER_CONFIGS must not be duplicated. Got {names}")
         return configs
 
     @property
@@ -453,14 +448,9 @@ def _other_versions_on_disk():
             other_version = version_dirs[0][0]
             if other_version != self.config.version:
                 warn_msg = (
-                    "Found a different version {other_version} of dataset {name} in "
-                    "cache_dir {cache_dir}. Using currently defined version "
-                    "{cur_version}.".format(
-                        other_version=str(other_version),
-                        name=self.name,
-                        cache_dir=self._cache_dir_root,
-                        cur_version=str(self.config.version),
-                    )
+                    f"Found a different version {str(other_version)} of dataset {self.name} in "
+                    f"cache_dir {self._cache_dir_root}. Using currently defined version "
+                    f"{str(self.config.version)}."
                 )
                 logger.warning(warn_msg)
 
@@ -537,22 +527,17 @@ def download_and_prepare(
         with FileLock(lock_path):
             data_exists = os.path.exists(self._cache_dir)
             if data_exists and download_mode == GenerateMode.REUSE_DATASET_IF_EXISTS:
-                logger.warning("Reusing dataset %s (%s)", self.name, self._cache_dir)
+                logger.warning(f"Reusing dataset {self.name} ({self._cache_dir})")
                 # We need to update the info in case some splits were added in the meantime
                 # for example when calling load_dataset from multiple workers.
                 self.info = self._load_info()
                 self.download_post_processing_resources(dl_manager)
                 return
-            logger.info("Generating dataset %s (%s)", self.name, self._cache_dir)
+            logger.info(f"Generating dataset {self.name} ({self._cache_dir})")
             if not is_remote_url(self._cache_dir_root):  # if cache dir is local, check for available space
                 if not utils.has_sufficient_disk_space(self.info.size_in_bytes or 0, directory=self._cache_dir_root):
                     raise IOError(
-                        "Not enough disk space. Needed: {} (download: {}, generated: {}, post-processed: {})".format(
-                            utils.size_str(self.info.size_in_bytes or 0),
-                            utils.size_str(self.info.download_size or 0),
-                            utils.size_str(self.info.dataset_size or 0),
-                            utils.size_str(self.info.post_processing_size or 0),
-                        )
+                        f"Not enough disk space. Needed: {utils.size_str(self.info.size_in_bytes or 0)} (download: {utils.size_str(self.info.download_size or 0)}, generated: {utils.size_str(self.info.dataset_size or 0)}, post-processed: {utils.size_str(self.info.post_processing_size or 0)})"
                     )
 
             @contextlib.contextmanager
@@ -647,14 +632,12 @@ def _download_prepared_from_hf_gcs(self, download_config: DownloadConfig):
         for split in self.info.splits:
             for resource_file_name in self._post_processing_resources(split).values():
                 if os.sep in resource_file_name:
-                    raise ValueError("Resources shouldn't be in a sub-directory: {}".format(resource_file_name))
+                    raise ValueError(f"Resources shouldn't be in a sub-directory: {resource_file_name}")
                 try:
                     resource_path = utils.cached_path(remote_cache_dir + "/" + resource_file_name)
                     shutil.move(resource_path, os.path.join(self._cache_dir, resource_file_name))
                 except ConnectionError:
-                    logger.info(
-                        "Couldn't download resourse file {} from Hf google storage.".format(resource_file_name)
-                    )
+                    logger.info(f"Couldn't download resourse file {resource_file_name} from Hf google storage.")
         logger.info("Dataset downloaded from Hf google storage.")
 
     def _download_and_prepare(self, dl_manager, verify_infos, **prepare_split_kwargs):
@@ -690,7 +673,7 @@ def _download_and_prepare(self, dl_manager, verify_infos, **prepare_split_kwargs
                     "._split_generator()."
                 )
 
-            logger.info("Generating split %s", split_generator.split_info.name)
+            logger.info(f"Generating split {split_generator.split_info.name}")
             split_dict.add(split_generator.split_info)
 
             try:
@@ -717,16 +700,14 @@ def download_post_processing_resources(self, dl_manager):
         for split in self.info.splits:
             for resource_name, resource_file_name in self._post_processing_resources(split).items():
                 if os.sep in resource_file_name:
-                    raise ValueError("Resources shouldn't be in a sub-directory: {}".format(resource_file_name))
+                    raise ValueError(f"Resources shouldn't be in a sub-directory: {resource_file_name}")
                 resource_path = os.path.join(self._cache_dir, resource_file_name)
                 if not os.path.exists(resource_path):
                     downloaded_resource_path = self._download_post_processing_resources(
                         split, resource_name, dl_manager
                     )
                     if downloaded_resource_path:
-                        logger.info(
-                            "Downloaded post-processing resource {} as {}".format(resource_name, resource_file_name)
-                        )
+                        logger.info(f"Downloaded post-processing resource {resource_name} as {resource_file_name}")
                         shutil.move(downloaded_resource_path, resource_path)
 
     def _load_info(self) -> DatasetInfo:
@@ -766,16 +747,13 @@ def as_dataset(
         if not os.path.exists(self._cache_dir):
             raise AssertionError(
                 (
-                    "Dataset %s: could not find data in %s. Please make sure to call "
+                    f"Dataset {self.name}: could not find data in {self._cache_dir_root}. Please make sure to call "
                     "builder.download_and_prepare(), or pass download=True to "
                     "datasets.load_dataset() before trying to access the Dataset object."
                 )
-                % (self.name, self._cache_dir_root)
             )
 
-        logger.debug(
-            "Constructing Dataset for split %s, from %s", split or ", ".join(self.info.splits), self._cache_dir
-        )
+        logger.debug(f'Constructing Dataset for split {split or ", ".join(self.info.splits)}, from {self._cache_dir}')
 
         # By default, return all splits
         if split is None:
@@ -820,7 +798,7 @@ def _build_single_dataset(
         if run_post_process:
             for resource_file_name in self._post_processing_resources(split).values():
                 if os.sep in resource_file_name:
-                    raise ValueError("Resources shouldn't be in a sub-directory: {}".format(resource_file_name))
+                    raise ValueError(f"Resources shouldn't be in a sub-directory: {resource_file_name}")
             resources_paths = {
                 resource_name: os.path.join(self._cache_dir, resource_file_name)
                 for resource_name, resource_file_name in self._post_processing_resources(split).items()
@@ -859,9 +837,7 @@ def _build_single_dataset(
                 if self.info.post_processed.features is not None:
                     if self.info.post_processed.features.type != ds.features.type:
                         raise ValueError(
-                            "Post-processed features info don't match the dataset:\nGot\n{}\nbut expected something like\n{}".format(
-                                self.info.post_processed.features, ds.features
-                            )
+                            f"Post-processed features info don't match the dataset:\nGot\n{self.info.post_processed.features}\nbut expected something like\n{ds.features}"
                         )
                     else:
                         ds.info.features = self.info.post_processed.features
@@ -1081,7 +1057,7 @@ def _generate_examples(self, **kwargs):
     def _prepare_split(self, split_generator):
         split_info = split_generator.split_info
 
-        fname = "{}-{}.arrow".format(self.name, split_generator.name)
+        fname = f"{self.name}-{split_generator.name}.arrow"
         fpath = os.path.join(self._cache_dir, fname)
 
         generator = self._generate_examples(**split_generator.gen_kwargs)
@@ -1149,7 +1125,7 @@ def _generate_tables(self, **kwargs):
         raise NotImplementedError()
 
     def _prepare_split(self, split_generator):
-        fname = "{}-{}.arrow".format(self.name, split_generator.name)
+        fname = f"{self.name}-{split_generator.name}.arrow"
         fpath = os.path.join(self._cache_dir, fname)
 
         generator = self._generate_tables(**split_generator.gen_kwargs)
@@ -1253,7 +1229,7 @@ def _download_and_prepare(self, dl_manager, verify_infos):
                 "\nIf you really want to run it locally because you feel like the "
                 "Dataset is small enough, you can use the local beam runner called "
                 "`DirectRunner` (you may run out of memory). \nExample of usage: "
-                "\n\t`{}`".format(usage_example)
+                f"\n\t`{usage_example}`"
             )
 
         beam_options = beam_options or beam.options.pipeline_options.PipelineOptions()
@@ -1304,7 +1280,7 @@ def _prepare_split(self, split_generator, pipeline):
         output_prefix = os.path.join(self._cache_dir, output_prefix)
 
         # To write examples to disk:
-        fname = "{}-{}.arrow".format(self.name, split_name)
+        fname = f"{self.name}-{split_name}.arrow"
         fpath = os.path.join(self._cache_dir, fname)
         beam_writer = BeamWriter(
             features=self.info.features, path=fpath, namespace=split_name, cache_dir=self._cache_dir
diff --git a/src/datasets/commands/convert.py b/src/datasets/commands/convert.py
index b3de12e4695..9a5df707617 100644
--- a/src/datasets/commands/convert.py
+++ b/src/datasets/commands/convert.py
@@ -88,7 +88,7 @@ def run(self):
 
         abs_datasets_path = os.path.abspath(self._datasets_directory)
 
-        self._logger.info("Converting datasets from %s to %s", abs_tfds_path, abs_datasets_path)
+        self._logger.info(f"Converting datasets from {abs_tfds_path} to {abs_datasets_path}")
 
         utils_files = []
         with_manual_update = []
@@ -100,7 +100,7 @@ def run(self):
             file_names = [os.path.basename(self._tfds_path)]
 
         for f_name in file_names:
-            self._logger.info("Looking at file %s", f_name)
+            self._logger.info(f"Looking at file {f_name}")
             input_file = os.path.join(abs_tfds_path, f_name)
             output_file = os.path.join(abs_datasets_path, f_name)
 
@@ -167,7 +167,7 @@ def run(self):
                 output_dir = os.path.join(abs_datasets_path, dir_name)
                 output_file = os.path.join(output_dir, f_name)
                 os.makedirs(output_dir, exist_ok=True)
-                self._logger.info("Adding directory %s", output_dir)
+                self._logger.info(f"Adding directory {output_dir}")
                 imports_to_builder_map.update({imp: output_dir for imp in tfds_imports})
             else:
                 # Utilities will be moved at the end
@@ -178,13 +178,13 @@ def run(self):
 
             with open(output_file, "w", encoding="utf-8") as f:
                 f.writelines(out_lines)
-            self._logger.info("Converted in %s", output_file)
+            self._logger.info(f"Converted in {output_file}")
 
         for utils_file in utils_files:
             try:
                 f_name = os.path.basename(utils_file)
                 dest_folder = imports_to_builder_map[f_name.replace(".py", "")]
-                self._logger.info("Moving %s to %s", utils_file, dest_folder)
+                self._logger.info(f"Moving {dest_folder} to {utils_file}")
                 shutil.copy(utils_file, dest_folder)
             except KeyError:
                 self._logger.error(f"Cannot find destination folder for {utils_file}. Please copy manually.")
diff --git a/src/datasets/commands/env.py b/src/datasets/commands/env.py
index 95eef550501..da29b3e93e5 100644
--- a/src/datasets/commands/env.py
+++ b/src/datasets/commands/env.py
@@ -32,4 +32,4 @@ def run(self):
 
     @staticmethod
     def format_dict(d):
-        return "\n".join(["- {}: {}".format(prop, val) for prop, val in d.items()]) + "\n"
+        return "\n".join([f"- {prop}: {val}" for prop, val in d.items()]) + "\n"
diff --git a/src/datasets/commands/run_beam.py b/src/datasets/commands/run_beam.py
index d48382b7ef6..a9d1a8c7433 100644
--- a/src/datasets/commands/run_beam.py
+++ b/src/datasets/commands/run_beam.py
@@ -91,7 +91,7 @@ def run(self):
         builders: List[DatasetBuilder] = []
         if self._beam_pipeline_options:
             beam_options = beam.options.pipeline_options.PipelineOptions(
-                flags=["--%s" % opt.strip() for opt in self._beam_pipeline_options.split(",") if opt]
+                flags=[f"--{opt.strip()}" for opt in self._beam_pipeline_options.split(",") if opt]
             )
         else:
             beam_options = None
@@ -147,10 +147,10 @@ def run(self):
             elif os.path.isfile(combined_path):
                 dataset_dir = path
             else:  # in case of a remote dataset
-                print("Dataset Infos file saved at {}".format(dataset_infos_path))
+                print(f"Dataset Infos file saved at {dataset_infos_path}")
                 exit(1)
 
             # Move datasetinfo back to the user
             user_dataset_infos_path = os.path.join(dataset_dir, config.DATASETDICT_INFOS_FILENAME)
             copyfile(dataset_infos_path, user_dataset_infos_path)
-            print("Dataset Infos file saved at {}".format(user_dataset_infos_path))
+            print(f"Dataset Infos file saved at {user_dataset_infos_path}")
diff --git a/src/datasets/commands/test.py b/src/datasets/commands/test.py
index 95d5e90c82e..988f83eac0a 100644
--- a/src/datasets/commands/test.py
+++ b/src/datasets/commands/test.py
@@ -169,13 +169,13 @@ def get_builders() -> Generator[DatasetBuilder, None, None]:
                     dataset_dir = path
                 else:  # in case of a remote dataset
                     dataset_dir = None
-                    print("Dataset Infos file saved at {}".format(dataset_infos_path))
+                    print(f"Dataset Infos file saved at {dataset_infos_path}")
 
                 # Move dataset_info back to the user
                 if dataset_dir is not None:
                     user_dataset_infos_path = os.path.join(dataset_dir, datasets.config.DATASETDICT_INFOS_FILENAME)
                     copyfile(dataset_infos_path, user_dataset_infos_path)
-                    print("Dataset Infos file saved at {}".format(user_dataset_infos_path))
+                    print(f"Dataset Infos file saved at {user_dataset_infos_path}")
 
             # If clear_cache=True, the download folder and the dataset builder cache directory are deleted
             if self._clear_cache:
diff --git a/src/datasets/config.py b/src/datasets/config.py
index 3815e0ef583..65efd253ff3 100644
--- a/src/datasets/config.py
+++ b/src/datasets/config.py
@@ -116,7 +116,7 @@
     try:
         BEAM_VERSION = version.parse(importlib_metadata.version("apache_beam"))
         BEAM_AVAILABLE = True
-        logger.info("Apache Beam version {} available.".format(BEAM_VERSION))
+        logger.info(f"Apache Beam version {BEAM_VERSION} available.")
     except importlib_metadata.PackageNotFoundError:
         pass
 else:
diff --git a/src/datasets/dataset_dict.py b/src/datasets/dataset_dict.py
index 78771fdea38..a3d8274223a 100644
--- a/src/datasets/dataset_dict.py
+++ b/src/datasets/dataset_dict.py
@@ -28,9 +28,7 @@ class DatasetDict(dict):
     def _check_values_type(self):
         for dataset in self.values():
             if not isinstance(dataset, Dataset):
-                raise TypeError(
-                    "Values in `DatasetDict` should of type `Dataset` but got type '{}'".format(type(dataset))
-                )
+                raise TypeError(f"Values in `DatasetDict` should of type `Dataset` but got type '{type(dataset)}'")
 
     def __getitem__(self, k) -> Dataset:
         if isinstance(k, (str, NamedSplit)) or len(self) == 0:
diff --git a/src/datasets/features/features.py b/src/datasets/features/features.py
index 3d33f558b9b..3551ca6de63 100644
--- a/src/datasets/features/features.py
+++ b/src/datasets/features/features.py
@@ -360,7 +360,7 @@ def __init__(self, shape: tuple, dtype: str):
         assert (
             self.ndims is not None and self.ndims > 1
         ), "You must instantiate an array type with a value for dim that is > 1"
-        assert len(shape) == self.ndims, "shape={} and ndims={} dom't match".format(shape, self.ndims)
+        assert len(shape) == self.ndims, f"shape={shape} and ndims={self.ndims} dom't match"
         self.shape = tuple(shape)
         self.value_type = dtype
         self.storage_dtype = self._generate_dtype(self.value_type)
@@ -475,7 +475,7 @@ def __from_arrow__(self, array):
         if array.type.shape[0] is None:
             raise NotImplementedError(
                 "Dynamic first dimension is not supported for "
-                "PandasArrayExtensionDtype, dimension: {}".format(array.type.shape)
+                f"PandasArrayExtensionDtype, dimension: {array.type.shape}"
             )
         zero_copy_only = _is_zero_copy_only(array.type)
         if isinstance(array, pa.ChunkedArray):
@@ -592,7 +592,7 @@ def __len__(self) -> int:
 
     def __eq__(self, other) -> np.ndarray:
         if not isinstance(other, PandasArrayExtensionArray):
-            raise NotImplementedError("Invalid type to compare to: {}".format(type(other)))
+            raise NotImplementedError(f"Invalid type to compare to: {type(other)}")
         return (self._data == other._data).all()
 
 
@@ -648,7 +648,7 @@ def __post_init__(self):
         elif self.num_classes != len(self.names):
             raise ValueError(
                 "ClassLabel number of names do not match the defined num_classes. "
-                "Got {} names VS {} num_classes".format(len(self.names), self.num_classes)
+                f"Got {len(self.names)} names VS {self.num_classes} num_classes"
             )
         # Prepare mappings
         self._int2str = [str(name) for name in self.names]
@@ -684,7 +684,7 @@ def str2int(self, values: Union[str, Iterable]):
                 except ValueError:
                     failed_parse = True
                 if failed_parse or not 0 <= value < self.num_classes:
-                    raise ValueError("Invalid string class label %s" % value)
+                    raise ValueError(f"Invalid string class label {value}")
         return output if return_list else output[0]
 
     def int2str(self, values: Union[int, Iterable]):
@@ -699,7 +699,7 @@ def int2str(self, values: Union[int, Iterable]):
 
         for v in values:
             if not 0 <= v < self.num_classes:
-                raise ValueError("Invalid integer class label %d" % v)
+                raise ValueError(f"Invalid integer class label {v:d}")
 
         if self._int2str:
             output = [self._int2str[int(v)] for v in values]
@@ -721,9 +721,7 @@ def encode_example(self, example_data):
 
         # Allowing -1 to mean no label.
         if not -1 <= example_data < self.num_classes:
-            raise ValueError(
-                "Class label %d greater than configured num_classes %d" % (example_data, self.num_classes)
-            )
+            raise ValueError(f"Class label {example_data:d} greater than configured num_classes {self.num_classes}")
         return example_data
 
     @staticmethod
@@ -837,7 +835,7 @@ def encode_nested_example(schema, obj):
                 return list_dict
         # schema.feature is not a dict
         if isinstance(obj, str):  # don't interpret a string as a list
-            raise ValueError("Got a string but expected a list instead: '{}'".format(obj))
+            raise ValueError(f"Got a string but expected a list instead: '{obj}'")
         if obj is None:
             return None
         else:
@@ -1044,7 +1042,7 @@ def encode_batch(self, batch):
         """
         encoded_batch = {}
         if set(batch) != set(self):
-            raise ValueError("Column mismatch between batch {} and features {}".format(set(batch), set(self)))
+            raise ValueError(f"Column mismatch between batch {set(batch)} and features {set(self)}")
         for key, column in batch.items():
             column = cast_to_python_objects(column)
             encoded_batch[key] = [encode_nested_example(self[key], obj) for obj in column]
diff --git a/src/datasets/features/translation.py b/src/datasets/features/translation.py
index b69b1172635..55ede0b9b20 100644
--- a/src/datasets/features/translation.py
+++ b/src/datasets/features/translation.py
@@ -97,9 +97,7 @@ def encode_example(self, translation_dict):
         lang_set = set(self.languages)
         if self.languages and set(translation_dict) - lang_set:
             raise ValueError(
-                "Some languages in example ({0}) are not in valid set ({1}).".format(
-                    ", ".join(sorted(set(translation_dict) - lang_set)), ", ".join(lang_set)
-                )
+                f'Some languages in example ({", ".join(sorted(set(translation_dict) - lang_set))}) are not in valid set ({", ".join(lang_set)}).'
             )
 
         # Convert dictionary into tuples, splitting out cases where there are
diff --git a/src/datasets/fingerprint.py b/src/datasets/fingerprint.py
index 04e9f3c8e72..63a77fb24d7 100644
--- a/src/datasets/fingerprint.py
+++ b/src/datasets/fingerprint.py
@@ -335,12 +335,12 @@ def fingerprint_transform(
             It should be in the format "MAJOR.MINOR.PATCH".
     """
 
-    assert use_kwargs is None or isinstance(use_kwargs, list), "use_kwargs is supposed to be a list, not {}".format(
-        type(use_kwargs)
-    )
+    assert use_kwargs is None or isinstance(
+        use_kwargs, list
+    ), f"use_kwargs is supposed to be a list, not {type(use_kwargs)}"
     assert ignore_kwargs is None or isinstance(
         ignore_kwargs, list
-    ), "ignore_kwargs is supposed to be a list, not {}".format(type(use_kwargs))
+    ), f"ignore_kwargs is supposed to be a list, not {type(use_kwargs)}"
     assert not inplace or not fingerprint_names, "fingerprint_names are only used when inplace is False"
     fingerprint_names = fingerprint_names if fingerprint_names is not None else ["new_fingerprint"]
 
@@ -348,10 +348,10 @@ def _fingerprint(func):
 
         assert inplace or all(  # check that not in-place functions require fingerprint parameters
             name in func.__code__.co_varnames for name in fingerprint_names
-        ), "function {} is missing parameters {} in signature".format(func, fingerprint_names)
+        ), f"function {func} is missing parameters {fingerprint_names} in signature"
         if randomized_function:  # randomized function have seed and generator parameters
-            assert "seed" in func.__code__.co_varnames, "'seed' must be in {}'s signature".format(func)
-            assert "generator" in func.__code__.co_varnames, "'generator' must be in {}'s signature".format(func)
+            assert "seed" in func.__code__.co_varnames, f"'seed' must be in {func}'s signature"
+            assert "generator" in func.__code__.co_varnames, f"'generator' must be in {func}'s signature"
         # this has to be outside the wrapper or since __qualname__ changes in multiprocessing
         transform = f"{func.__module__}.{func.__qualname__}"
         if version is not None:
diff --git a/src/datasets/formatting/__init__.py b/src/datasets/formatting/__init__.py
index 325c4bd4866..5baf208f6cd 100644
--- a/src/datasets/formatting/__init__.py
+++ b/src/datasets/formatting/__init__.py
@@ -124,7 +124,5 @@ def get_formatter(format_type: Optional[str], **format_kwargs) -> Formatter:
         raise _FORMAT_TYPES_ALIASES_UNAVAILABLE[format_type]
     else:
         raise ValueError(
-            "Return type should be None or selected in {}, but got '{}'".format(
-                list(type for type in _FORMAT_TYPES.keys() if type != None), format_type
-            )
+            f"Return type should be None or selected in {list(type for type in _FORMAT_TYPES.keys() if type != None)}, but got '{format_type}'"
         )
diff --git a/src/datasets/formatting/formatting.py b/src/datasets/formatting/formatting.py
index 6ab00073bcb..1c92778e604 100644
--- a/src/datasets/formatting/formatting.py
+++ b/src/datasets/formatting/formatting.py
@@ -396,7 +396,7 @@ def format_batch(self, pa_table: pa.Table) -> dict:
 
 def _check_valid_column_key(key: str, columns: List[str]) -> None:
     if key not in columns:
-        raise KeyError("Column {} not in the dataset. Current columns in the dataset: {}".format(key, columns))
+        raise KeyError(f"Column {key} not in the dataset. Current columns in the dataset: {columns}")
 
 
 def _check_valid_index_key(key: Union[int, slice, range, Iterable], size: int) -> None:
diff --git a/src/datasets/info.py b/src/datasets/info.py
index 520040bb603..adf49ca6fd3 100644
--- a/src/datasets/info.py
+++ b/src/datasets/info.py
@@ -247,7 +247,7 @@ def from_directory(cls, dataset_info_dir: str) -> "DatasetInfo":
             dataset_info_dir (`str`): The directory containing the metadata file. This
                 should be the root directory of a specific dataset version.
         """
-        logger.info("Loading Dataset info from %s", dataset_info_dir)
+        logger.info(f"Loading Dataset info from {dataset_info_dir}")
         if not dataset_info_dir:
             raise ValueError("Calling DatasetInfo.from_directory() with undefined dataset_info_dir.")
 
@@ -279,17 +279,17 @@ def write_to_directory(self, dataset_infos_dir, overwrite=False):
         total_dataset_infos = {}
         dataset_infos_path = os.path.join(dataset_infos_dir, config.DATASETDICT_INFOS_FILENAME)
         if os.path.exists(dataset_infos_path) and not overwrite:
-            logger.info("Dataset Infos already exists in {}. Completing it with new infos.".format(dataset_infos_dir))
+            logger.info(f"Dataset Infos already exists in {dataset_infos_dir}. Completing it with new infos.")
             total_dataset_infos = self.from_directory(dataset_infos_dir)
         else:
-            logger.info("Writing new Dataset Infos in {}".format(dataset_infos_dir))
+            logger.info(f"Writing new Dataset Infos in {dataset_infos_dir}")
         total_dataset_infos.update(self)
         with open(dataset_infos_path, "w", encoding="utf-8") as f:
             json.dump({config_name: asdict(dset_info) for config_name, dset_info in total_dataset_infos.items()}, f)
 
     @classmethod
     def from_directory(cls, dataset_infos_dir):
-        logger.info("Loading Dataset Infos from {}".format(dataset_infos_dir))
+        logger.info(f"Loading Dataset Infos from {dataset_infos_dir}")
         with open(os.path.join(dataset_infos_dir, config.DATASETDICT_INFOS_FILENAME), "r", encoding="utf-8") as f:
             dataset_infos_dict = {
                 config_name: DatasetInfo.from_dict(dataset_info_dict)
@@ -354,7 +354,7 @@ def from_directory(cls, metric_info_dir) -> "MetricInfo":
             metric_info_dir: `str` The directory containing the metadata file. This
                 should be the root directory of a specific dataset version.
         """
-        logger.info("Loading Metric info from %s", metric_info_dir)
+        logger.info(f"Loading Metric info from {metric_info_dir}")
         if not metric_info_dir:
             raise ValueError("Calling MetricInfo.from_directory() with undefined metric_info_dir.")
 
diff --git a/src/datasets/load.py b/src/datasets/load.py
index 2310c92122f..6b2a90322b3 100644
--- a/src/datasets/load.py
+++ b/src/datasets/load.py
@@ -159,7 +159,7 @@ def convert_github_url(url_path: str) -> Tuple[str, Optional[str]]:
             github_path = parsed.path[1:]
             repo_info, branch = github_path.split("/tree/") if "/tree/" in github_path else (github_path, "master")
             repo_owner, repo_name = repo_info.split("/")
-            url_path = "https://github.com/{}/{}/archive/{}.zip".format(repo_owner, repo_name, branch)
+            url_path = f"https://github.com/{repo_owner}/{repo_name}/archive/{branch}.zip"
             sub_directory = f"{repo_name}-{branch}"
     return url_path, sub_directory
 
@@ -197,7 +197,7 @@ def get_imports(file_path: str) -> Tuple[str, str, str, str]:
     with open(file_path, mode="r", encoding="utf-8") as f:
         lines.extend(f.readlines())
 
-    logger.debug("Checking %s for additional imports.", file_path)
+    logger.debug(f"Checking {file_path} for additional imports.")
     imports: List[Tuple[str, str, str, Optional[str]]] = []
     is_in_docstring = False
     for line in lines:
@@ -1712,12 +1712,12 @@ def load_from_disk(dataset_path: str, fs=None, keep_in_memory: Optional[bool] =
         dest_dataset_path = dataset_path
 
     if not fs.exists(dest_dataset_path):
-        raise FileNotFoundError("Directory {} not found".format(dataset_path))
+        raise FileNotFoundError(f"Directory {dataset_path} not found")
     if fs.isfile(Path(dest_dataset_path, config.DATASET_INFO_FILENAME).as_posix()):
         return Dataset.load_from_disk(dataset_path, fs, keep_in_memory=keep_in_memory)
     elif fs.isfile(Path(dest_dataset_path, config.DATASETDICT_JSON_FILENAME).as_posix()):
         return DatasetDict.load_from_disk(dataset_path, fs, keep_in_memory=keep_in_memory)
     else:
         raise FileNotFoundError(
-            "Directory {} is neither a dataset directory nor a dataset dict directory.".format(dataset_path)
+            f"Directory {dataset_path} is neither a dataset directory nor a dataset dict directory."
         )
diff --git a/src/datasets/naming.py b/src/datasets/naming.py
index c0625dd7339..5ccdfbc6096 100644
--- a/src/datasets/naming.py
+++ b/src/datasets/naming.py
@@ -46,30 +46,30 @@ def snakecase_to_camelcase(name):
 
 def filename_prefix_for_name(name):
     if os.path.basename(name) != name:
-        raise ValueError("Should be a dataset name, not a path: %s" % name)
+        raise ValueError(f"Should be a dataset name, not a path: {name}")
     return camelcase_to_snakecase(name)
 
 
 def filename_prefix_for_split(name, split):
     if os.path.basename(name) != name:
-        raise ValueError("Should be a dataset name, not a path: %s" % name)
+        raise ValueError(f"Should be a dataset name, not a path: {name}")
     if not re.match(_split_re, split):
         raise ValueError(f"Split name should match '{_split_re}'' but got '{split}'.")
-    return "%s-%s" % (filename_prefix_for_name(name), split)
+    return f"{filename_prefix_for_name(name)}-{split}"
 
 
 def filepattern_for_dataset_split(dataset_name, split, data_dir, filetype_suffix=None):
     prefix = filename_prefix_for_split(dataset_name, split)
     if filetype_suffix:
-        prefix += ".%s" % filetype_suffix
+        prefix += f".{filetype_suffix}"
     filepath = os.path.join(data_dir, prefix)
-    return "%s*" % filepath
+    return f"{filepath}*"
 
 
 def filename_for_dataset_split(dataset_name, split, filetype_suffix=None):
     prefix = filename_prefix_for_split(dataset_name, split)
     if filetype_suffix:
-        prefix += ".%s" % filetype_suffix
+        prefix += f".{filetype_suffix}"
     return prefix
 
 
diff --git a/src/datasets/search.py b/src/datasets/search.py
index 08f6949f6f0..f235055fe5a 100644
--- a/src/datasets/search.py
+++ b/src/datasets/search.py
@@ -168,7 +168,7 @@ def passage_generator():
             logger.warning(
                 f"Some documents failed to be added to ElasticSearch. Failures: {len(documents)-successes}/{len(documents)}"
             )
-        logger.info("Indexed %d documents" % (successes,))
+        logger.info(f"Indexed {successes:d} documents")
 
     def search(self, query: str, k=10) -> SearchResults:
         """Find the nearest examples indices to the query.
@@ -267,7 +267,7 @@ def add_vectors(
                 self.faiss_res = faiss.StandardGpuResources()
                 index = faiss.index_cpu_to_gpu(self.faiss_res, self.device, index)
             self.faiss_index = index
-            logger.info("Created faiss index of type {}".format(type(self.faiss_index)))
+            logger.info(f"Created faiss index of type {type(self.faiss_index)}")
 
         # Set verbosity level
         if faiss_verbose is not None:
@@ -282,13 +282,13 @@ def add_vectors(
         # Train
         if train_size is not None:
             train_vecs = vectors[:train_size] if column is None else vectors[:train_size][column]
-            logger.info("Training the index with the first {} vectors".format(len(train_vecs)))
+            logger.info(f"Training the index with the first {len(train_vecs)} vectors")
             self.faiss_index.train(train_vecs)
         else:
             logger.info("Ignored the training step of the faiss index as `train_size` is None.")
 
         # Add vectors
-        logger.info("Adding {} vectors to the faiss index".format(len(vectors)))
+        logger.info(f"Adding {len(vectors)} vectors to the faiss index")
         for i in utils.tqdm(
             range(0, len(vectors), batch_size), disable=bool(logging.get_verbosity() == logging.NOTSET)
         ):
@@ -474,9 +474,9 @@ def save_faiss_index(self, index_name: str, file: Union[str, PurePath]):
         """
         index = self.get_index(index_name)
         if not isinstance(index, FaissIndex):
-            raise ValueError("Index '{}' is not a FaissIndex but a '{}'".format(index_name, type(index)))
+            raise ValueError(f"Index '{index_name}' is not a FaissIndex but a '{type(index)}'")
         index.save(file)
-        logger.info("Saved FaissIndex {} at {}".format(index_name, file))
+        logger.info(f"Saved FaissIndex {index_name} at {file}")
 
     def load_faiss_index(
         self,
@@ -498,11 +498,9 @@ def load_faiss_index(
         index = FaissIndex.load(file, device=device)
         assert index.faiss_index.ntotal == len(
             self
-        ), "Index size should match Dataset size, but Index '{}' at {} has {} elements while the dataset has {} examples.".format(
-            index_name, file, index.faiss_index.ntotal, len(self)
-        )
+        ), f"Index size should match Dataset size, but Index '{index_name}' at {file} has {index.faiss_index.ntotal} elements while the dataset has {len(self)} examples."
         self._indexes[index_name] = index
-        logger.info("Loaded FaissIndex {} from {}".format(index_name, file))
+        logger.info(f"Loaded FaissIndex {index_name} from {file}")
 
     def add_elasticsearch_index(
         self,
diff --git a/src/datasets/splits.py b/src/datasets/splits.py
index 3e4fadb2e01..56b727e1848 100644
--- a/src/datasets/splits.py
+++ b/src/datasets/splits.py
@@ -193,8 +193,8 @@ def subsplit(self, arg=None, k=None, percent=None, weighted=None):  # pylint: di
 
         if not (k or percent or weighted):
             raise ValueError(
-                "Invalid split argument {}. Only list, slice and int supported. "
-                "One of k, weighted or percent should be set to a non empty value.".format(arg)
+                f"Invalid split argument {arg}. Only list, slice and int supported. "
+                "One of k, weighted or percent should be set to a non empty value."
             )
 
         def assert_slices_coverage(slices):
@@ -203,7 +203,7 @@ def assert_slices_coverage(slices):
 
         if k:
             if not 0 < k <= 100:
-                raise ValueError("Subsplit k should be between 0 and 100, got {}".format(k))
+                raise ValueError(f"Subsplit k should be between 0 and 100, got {k}")
             shift = 100 // k
             slices = [slice(i * shift, (i + 1) * shift) for i in range(k)]
             # Round up last element to ensure all elements are taken
@@ -244,7 +244,7 @@ def assert_slices_coverage(slices):
 class PercentSliceMeta(type):
     def __getitem__(cls, slice_value):
         if not isinstance(slice_value, slice):
-            raise ValueError("datasets.percent should only be called with slice, not {}".format(slice_value))
+            raise ValueError(f"datasets.percent should only be called with slice, not {slice_value}")
         return slice_value
 
 
@@ -276,7 +276,7 @@ def get_read_instruction(self, split_dict):
         return read_instruction1 + read_instruction2
 
     def __repr__(self):
-        return "({!r} + {!r})".format(self._split1, self._split2)
+        return f"({repr(self._split1)} + {repr(self._split2)})"
 
 
 class _SubSplit(SplitBase):
@@ -298,7 +298,7 @@ def __repr__(self):
             stop="" if self._slice_value.stop is None else self._slice_value.stop,
             step=self._slice_value.step,
         )
-        return "{!r}(datasets.percent[{}])".format(self._split, slice_str)
+        return f"{repr(self._split)}(datasets.percent[{slice_str}])"
 
 
 class NamedSplit(SplitBase):
@@ -361,7 +361,7 @@ def __eq__(self, other):
         elif isinstance(other, str):  # Other should be string
             return self._name == other
         else:
-            raise ValueError("Equality not supported between split {} and {}".format(self, other))
+            raise ValueError(f"Equality not supported between split {self} and {other}")
 
     def __hash__(self):
         return hash(self._name)
@@ -470,7 +470,7 @@ def __getitem__(self, slice_value):
         split_instruction = SplitReadInstruction()
         for v in self._splits.values():
             if v.slice_value is not None:
-                raise ValueError("Trying to slice Split {} which has already been sliced".format(v.split_info.name))
+                raise ValueError(f"Trying to slice Split {v.split_info.name} which has already been sliced")
             v = v._asdict()
             v["slice_value"] = slice_value
             split_instruction.add(SlicedSplitInfo(**v))
@@ -502,15 +502,15 @@ def __getitem__(self, key: Union[SplitBase, str]):
 
     def __setitem__(self, key: Union[SplitBase, str], value: SplitInfo):
         if key != value.name:
-            raise ValueError("Cannot add elem. (key mismatch: '{}' != '{}')".format(key, value.name))
+            raise ValueError(f"Cannot add elem. (key mismatch: '{key}' != '{value.name}')")
         if key in self:
-            raise ValueError("Split {} already present".format(key))
+            raise ValueError(f"Split {key} already present")
         super(SplitDict, self).__setitem__(key, value)
 
     def add(self, split_info: SplitInfo):
         """Add the split info."""
         if split_info.name in self:
-            raise ValueError("Split {} already present".format(split_info.name))
+            raise ValueError(f"Split {split_info.name} already present")
         split_info.dataset_name = self.dataset_name
         super(SplitDict, self).__setitem__(split_info.name, split_info)
 
diff --git a/src/datasets/utils/beam_utils.py b/src/datasets/utils/beam_utils.py
index e9c9941bb09..32ee89c2d64 100644
--- a/src/datasets/utils/beam_utils.py
+++ b/src/datasets/utils/beam_utils.py
@@ -29,9 +29,9 @@ def upload_local_to_remote(local_file_path, remote_file_path, force_upload=False
     fs = FileSystems
     if fs.exists(remote_file_path):
         if force_upload:
-            logger.info("Remote path already exist: {}. Overwriting it as force_upload=True.".format(remote_file_path))
+            logger.info(f"Remote path already exist: {remote_file_path}. Overwriting it as force_upload=True.")
         else:
-            logger.info("Remote path already exist: {}. Skipping it as force_upload=False.".format(remote_file_path))
+            logger.info(f"Remote path already exist: {remote_file_path}. Skipping it as force_upload=False.")
             return
     with fs.create(remote_file_path) as remote_file:
         with open(local_file_path, "rb") as local_file:
@@ -46,9 +46,9 @@ def download_remote_to_local(remote_file_path, local_file_path, force_download=F
     fs = FileSystems
     if os.path.exists(local_file_path):
         if force_download:
-            logger.info("Local path already exist: {}. Overwriting it as force_upload=True.".format(remote_file_path))
+            logger.info(f"Local path already exist: {remote_file_path}. Overwriting it as force_upload=True.")
         else:
-            logger.info("Local path already exist: {}. Skipping it as force_upload=False.".format(remote_file_path))
+            logger.info(f"Local path already exist: {remote_file_path}. Skipping it as force_upload=False.")
             return
     with fs.open(remote_file_path) as remote_file:
         with open(local_file_path, "wb") as local_file:
@@ -112,7 +112,7 @@ def __init__(
 
         .. testcleanup::
 
-            for output in glob.glob('{}*'.format(filename)):
+            for output in glob.glob(f'{filename}*'):
                 os.remove(output)
 
         For more information on supported types and schema, please see the pyarrow
diff --git a/src/datasets/utils/download_manager.py b/src/datasets/utils/download_manager.py
index 3678b2884b6..c43af5d6262 100644
--- a/src/datasets/utils/download_manager.py
+++ b/src/datasets/utils/download_manager.py
@@ -116,9 +116,7 @@ def upload(local_file_path):
                 remote_dir, config.DOWNLOADED_DATASETS_DIR, os.path.basename(local_file_path)
             )
             logger.info(
-                "Uploading {} ({}) to {}.".format(
-                    local_file_path, size_str(os.path.getsize(local_file_path)), remote_file_path
-                )
+                f"Uploading {local_file_path} ({size_str(os.path.getsize(local_file_path))}) to {remote_file_path}."
             )
             upload_local_to_remote(local_file_path, remote_file_path)
             return remote_file_path
@@ -198,7 +196,7 @@ def download(self, url_or_urls):
             download_func, url_or_urls, map_tuple=True, num_proc=download_config.num_proc, disable_tqdm=False
         )
         duration = datetime.now() - start_time
-        logger.info("Downloading took {} min".format(duration.total_seconds() // 60))
+        logger.info(f"Downloading took {duration.total_seconds() // 60} min")
         url_or_urls = NestedDataStructure(url_or_urls)
         downloaded_path_or_paths = NestedDataStructure(downloaded_path_or_paths)
         self.downloaded_paths.update(dict(zip(url_or_urls.flatten(), downloaded_path_or_paths.flatten())))
@@ -206,7 +204,7 @@ def download(self, url_or_urls):
         start_time = datetime.now()
         self._record_sizes_checksums(url_or_urls, downloaded_path_or_paths)
         duration = datetime.now() - start_time
-        logger.info("Checksum Computation took {} min".format(duration.total_seconds() // 60))
+        logger.info(f"Checksum Computation took {duration.total_seconds() // 60} min")
 
         return downloaded_path_or_paths.data
 
diff --git a/src/datasets/utils/file_utils.py b/src/datasets/utils/file_utils.py
index c7c70bc4e1a..31d433f0897 100644
--- a/src/datasets/utils/file_utils.py
+++ b/src/datasets/utils/file_utils.py
@@ -309,10 +309,10 @@ def cached_path(
         output_path = url_or_filename
     elif is_local_path(url_or_filename):
         # File, but it doesn't exist.
-        raise FileNotFoundError("Local file {} doesn't exist".format(url_or_filename))
+        raise FileNotFoundError(f"Local file {url_or_filename} doesn't exist")
     else:
         # Something unknown
-        raise ValueError("unable to parse {} as a URL or as a local path".format(url_or_filename))
+        raise ValueError(f"unable to parse {url_or_filename} as a URL or as a local path")
 
     if output_path is None:
         return output_path
@@ -326,18 +326,18 @@ def cached_path(
 
 
 def get_datasets_user_agent(user_agent: Optional[Union[str, dict]] = None) -> str:
-    ua = "datasets/{}; python/{}".format(__version__, config.PY_VERSION)
-    ua += "; pyarrow/{}".format(config.PYARROW_VERSION)
+    ua = f"datasets/{__version__}; python/{config.PY_VERSION}"
+    ua += f"; pyarrow/{config.PYARROW_VERSION}"
     if config.TORCH_AVAILABLE:
-        ua += "; torch/{}".format(config.TORCH_VERSION)
+        ua += f"; torch/{config.TORCH_VERSION}"
     if config.TF_AVAILABLE:
-        ua += "; tensorflow/{}".format(config.TF_VERSION)
+        ua += f"; tensorflow/{config.TF_VERSION}"
     if config.JAX_AVAILABLE:
-        ua += "; jax/{}".format(config.JAX_VERSION)
+        ua += f"; jax/{config.JAX_VERSION}"
     if config.BEAM_AVAILABLE:
-        ua += "; apache_beam/{}".format(config.BEAM_VERSION)
+        ua += f"; apache_beam/{config.BEAM_VERSION}"
     if isinstance(user_agent, dict):
-        ua += "; " + "; ".join("{}/{}".format(k, v) for k, v in user_agent.items())
+        ua += f"; {'; '.join(f'{k}/{v}' for k, v in user_agent.items())}"
     elif isinstance(user_agent, str):
         ua += "; " + user_agent
     return ua
@@ -355,7 +355,7 @@ def get_authentication_headers_for_url(url: str, use_auth_token: Optional[Union[
 
             token = hf_api.HfFolder.get_token()
         if token:
-            headers["authorization"] = "Bearer {}".format(token)
+            headers["authorization"] = f"Bearer {token}"
     return headers
 
 
@@ -434,7 +434,7 @@ def http_get(url, temp_file, proxies=None, resume_size=0, headers=None, cookies=
     headers = copy.deepcopy(headers) or {}
     headers["user-agent"] = get_datasets_user_agent(user_agent=headers.get("user-agent"))
     if resume_size > 0:
-        headers["Range"] = "bytes=%d-" % (resume_size,)
+        headers["Range"] = f"bytes={resume_size:d}-"
     response = _request_with_retry(
         method="GET",
         url=url,
@@ -577,7 +577,7 @@ def get_from_cache(
                 or (response.status_code == 403 and "ndownloader.figstatic.com" in url)
             ):
                 connected = True
-                logger.info("Couldn't get ETag version for url {}".format(url))
+                logger.info(f"Couldn't get ETag version for url {url}")
         except (EnvironmentError, requests.exceptions.Timeout):
             # not connected
             pass
@@ -593,9 +593,9 @@ def get_from_cache(
                 " disabled. To enable file online look-ups, set 'local_files_only' to False."
             )
         elif response is not None and response.status_code == 404:
-            raise FileNotFoundError("Couldn't find file at {}".format(url))
+            raise FileNotFoundError(f"Couldn't find file at {url}")
         _raise_if_offline_mode_is_enabled(f"Tried to reach {url}")
-        raise ConnectionError("Couldn't reach {}".format(url))
+        raise ConnectionError(f"Couldn't reach {url}")
 
     # Try a second time
     filename = hash_url_to_filename(original_url, etag)
@@ -629,7 +629,7 @@ def _resumable_file_manager():
         # Download to temporary file, then copy to cache dir once finished.
         # Otherwise you get corrupt cache entries if the download gets interrupted.
         with temp_file_manager() as temp_file:
-            logger.info("%s not found in cache or force_download set to True, downloading to %s", url, temp_file.name)
+            logger.info(f"{url} not found in cache or force_download set to True, downloading to {temp_file.name}")
 
             # GET file object
             if url.startswith("ftp://"):
@@ -645,10 +645,10 @@ def _resumable_file_manager():
                     max_retries=max_retries,
                 )
 
-        logger.info("storing %s in cache at %s", url, cache_path)
+        logger.info(f"storing {url} in cache at {cache_path}")
         shutil.move(temp_file.name, cache_path)
 
-        logger.info("creating metadata file for %s", cache_path)
+        logger.info(f"creating metadata file for {cache_path}")
         meta = {"url": url, "etag": etag}
         meta_path = cache_path + ".json"
         with open(meta_path, "w", encoding="utf-8") as meta_file:
diff --git a/src/datasets/utils/filelock.py b/src/datasets/utils/filelock.py
index 34286fc4e9e..ace6673abef 100644
--- a/src/datasets/utils/filelock.py
+++ b/src/datasets/utils/filelock.py
@@ -99,7 +99,7 @@ def __init__(self, lock_file):
         return None
 
     def __str__(self):
-        temp = "The file lock '{}' could not be acquired.".format(self.lock_file)
+        temp = f"The file lock '{self.lock_file}' could not be acquired."
         return temp
 
 
@@ -269,18 +269,18 @@ def acquire(self, timeout=None, poll_intervall=0.05):
             while True:
                 with self._thread_lock:
                     if not self.is_locked:
-                        logger().debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
+                        logger().debug(f"Attempting to acquire lock {lock_id} on {lock_filename}")
                         self._acquire()
 
                 if self.is_locked:
-                    logger().debug("Lock %s acquired on %s", lock_id, lock_filename)
+                    logger().debug(f"Lock {lock_id} acquired on {lock_filename}")
                     break
                 elif timeout >= 0 and time.time() - start_time > timeout:
-                    logger().debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
+                    logger().debug(f"Timeout on acquiring lock {lock_id} on {lock_filename}")
                     raise Timeout(self._lock_file)
                 else:
                     logger().debug(
-                        "Lock %s not acquired on %s, waiting %s seconds ...", lock_id, lock_filename, poll_intervall
+                        f"Lock {lock_id} not acquired on {lock_filename}, waiting {poll_intervall} seconds ..."
                     )
                     time.sleep(poll_intervall)
         except:  # noqa
@@ -313,10 +313,10 @@ def release(self, force=False):
                     lock_id = id(self)
                     lock_filename = self._lock_file
 
-                    logger().debug("Attempting to release lock %s on %s", lock_id, lock_filename)
+                    logger().debug(f"Attempting to release lock {lock_id} on {lock_filename}")
                     self._release()
                     self._lock_counter = 0
-                    logger().debug("Lock %s released on %s", lock_id, lock_filename)
+                    logger().debug(f"Lock {lock_id} released on {lock_filename}")
 
         return None
 
diff --git a/src/datasets/utils/py_utils.py b/src/datasets/utils/py_utils.py
index d7f76145bda..a297900c059 100644
--- a/src/datasets/utils/py_utils.py
+++ b/src/datasets/utils/py_utils.py
@@ -78,8 +78,8 @@ def size_str(size_in_bytes):
     for (name, size_bytes) in _NAME_LIST:
         value = size_in_bytes / size_bytes
         if value >= 1.0:
-            return "{:.2f} {}".format(value, name)
-    return "{} {}".format(int(size_in_bytes), "bytes")
+            return f"{value:.2f} {name}"
+    return f"{int(size_in_bytes)} bytes"
 
 
 def string_to_dict(string: str, pattern: str) -> Dict[str, str]:
@@ -259,18 +259,16 @@ def map_nested(
             f"length: {sum(len(i[1]) for i in split_kwds)}"
         )
         logger.info(
-            "Spawning {} processes for {} objects in slices of {}".format(
-                num_proc, len(iterable), [len(i[1]) for i in split_kwds]
-            )
+            f"Spawning {num_proc} processes for {len(iterable)} objects in slices of {[len(i[1]) for i in split_kwds]}"
         )
         initargs, initializer = None, None
         if not disable_tqdm:
             initargs, initializer = (RLock(),), tqdm.set_lock
         with Pool(num_proc, initargs=initargs, initializer=initializer) as pool:
             mapped = pool.map(_single_map_nested, split_kwds)
-        logger.info("Finished {} processes".format(num_proc))
+        logger.info(f"Finished {num_proc} processes")
         mapped = [obj for proc_res in mapped for obj in proc_res]
-        logger.info("Unpacked {} objects".format(len(mapped)))
+        logger.info(f"Unpacked {len(mapped)} objects")
 
     if isinstance(data_struct, dict):
         return dict(zip(data_struct.keys(), mapped))
@@ -305,7 +303,7 @@ def flatten_nest_dict(d):
     flat_dict = NonMutableDict()
     for k, v in d.items():
         if isinstance(v, dict):
-            flat_dict.update({"{}/{}".format(k, k2): v2 for k2, v2 in flatten_nest_dict(v).items()})
+            flat_dict.update({f"{k}/{k2}": v2 for k2, v2 in flatten_nest_dict(v).items()})
         else:
             flat_dict[k] = v
     return flat_dict
@@ -433,7 +431,7 @@ def _save_parametrized_type_hint(pickler, obj):
             else:
                 initargs = (obj.__origin__, (list(args[:-1]), args[-1]))
         else:  # pragma: no cover
-            raise pickle.PicklingError("Datasets pickle Error: Unknown type {}".format(type(obj)))
+            raise pickle.PicklingError(f"Datasets pickle Error: Unknown type {type(obj)}")
         pickler.save_reduce(_CloudPickleTypeHintFix._create_parametrized_type_hint, initargs, obj=obj)
 
 
@@ -444,7 +442,7 @@ def _save_code(pickler, obj):
     This is a modified version that removes the origin (filename + line no.)
     of functions created in notebooks or shells for example.
     """
-    dill._dill.log.info("Co: %s" % obj)
+    dill._dill.log.info(f"Co: {obj}")
     # The filename of a function is the .py file where it is defined.
     # Filenames of functions created in notebooks or shells start with '<'
     # ex: <ipython-input-13-9ed2afe61d25> for ipython, and <stdin> for shell
@@ -532,7 +530,7 @@ def save_function(pickler, obj):
     the keys in the output dictionary of globalvars can change.
     """
     if not dill._dill._locate_function(obj):
-        dill._dill.log.info("F1: %s" % obj)
+        dill._dill.log.info(f"F1: {obj}")
         if getattr(pickler, "_recurse", False):
             # recurse to get all globals referred to by obj
             globalvars = dill.detect.globalvars
@@ -591,7 +589,7 @@ def save_function(pickler, obj):
             pickler.clear_memo()
         dill._dill.log.info("# F1")
     else:
-        dill._dill.log.info("F2: %s" % obj)
+        dill._dill.log.info(f"F2: {obj}")
         name = getattr(obj, "__qualname__", getattr(obj, "__name__", None))
         dill._dill.StockPickler.save_global(pickler, obj, name=name)
         dill._dill.log.info("# F2")
@@ -609,7 +607,7 @@ def copyfunc(func):
 
     @pklregister(type(regex.Regex("", 0)))
     def _save_regex(pickler, obj):
-        dill._dill.log.info("Re: %s" % obj)
+        dill._dill.log.info(f"Re: {obj}")
         args = (
             obj.pattern,
             obj.flags,
diff --git a/src/datasets/utils/version.py b/src/datasets/utils/version.py
index 0e4c0382776..95b8fbb0dde 100644
--- a/src/datasets/utils/version.py
+++ b/src/datasets/utils/version.py
@@ -53,7 +53,7 @@ def __post_init__(self):
         self.major, self.minor, self.patch = _str_to_version(self.version_str)
 
     def __repr__(self):
-        return "{}.{}.{}".format(*self.tuple)
+        return f"{self.tuple[0]}.{self.tuple[1]}.{self.tuple[2]}"
 
     @property
     def tuple(self):
@@ -64,7 +64,7 @@ def _validate_operand(self, other):
             return Version(other)
         elif isinstance(other, Version):
             return other
-        raise AssertionError("{} (type {}) cannot be compared to version.".format(other, type(other)))
+        raise AssertionError(f"{other} (type {type(other)}) cannot be compared to version.")
 
     def __eq__(self, other):
         other = self._validate_operand(other)
@@ -111,7 +111,7 @@ def _str_to_version(version_str, allow_wildcard=False):
     reg = _VERSION_WILDCARD_REG if allow_wildcard else _VERSION_RESOLVED_REG
     res = reg.match(version_str)
     if not res:
-        msg = "Invalid version '{}'. Format should be x.y.z".format(version_str)
+        msg = f"Invalid version '{version_str}'. Format should be x.y.z"
         if allow_wildcard:
             msg += " with {x,y,z} being digits or wildcard."
         else:
diff --git a/templates/new_metric_script.py b/templates/new_metric_script.py
index 6301340b0e5..17a5fa819f4 100644
--- a/templates/new_metric_script.py
+++ b/templates/new_metric_script.py
@@ -97,7 +97,7 @@ def _compute(self, predictions, references):
             second_score = sum(abs(len(i) - len(j)) for i, j in zip(predictions, references) if i not in self.bad_words)
             second_score /= sum(i not in self.bad_words for i in predictions)
         else:
-            raise ValueError("Invalid config name for NewMetric: {}. Please use 'max' or 'mean'.".format(self.config_name))
+            raise ValueError(f"Invalid config name for NewMetric: {self.config_name}. Please use 'max' or 'mean'.")
 
         return {
             "accuracy": accuracy,
diff --git a/tests/hub_fixtures.py b/tests/hub_fixtures.py
index c870b96d1de..03198e21867 100644
--- a/tests/hub_fixtures.py
+++ b/tests/hub_fixtures.py
@@ -31,7 +31,7 @@ def hf_token(hf_api: HfApi):
 
 @pytest.fixture(scope="session")
 def hf_private_dataset_repo_txt_data_(hf_api: HfApi, hf_token, text_file):
-    repo_name = "repo_txt_data-{}".format(int(time.time() * 10e3))
+    repo_name = f"repo_txt_data-{int(time.time() * 10e3)}"
     hf_api.create_repo(token=hf_token, name=repo_name, repo_type="dataset", private=True)
     repo_id = f"{USER}/{repo_name}"
     hf_api.upload_file(
@@ -57,7 +57,7 @@ def hf_private_dataset_repo_txt_data(hf_private_dataset_repo_txt_data_):
 
 @pytest.fixture(scope="session")
 def hf_private_dataset_repo_zipped_txt_data_(hf_api: HfApi, hf_token, zip_csv_path):
-    repo_name = "repo_zipped_txt_data-{}".format(int(time.time() * 10e3))
+    repo_name = f"repo_zipped_txt_data-{int(time.time() * 10e3)}"
     hf_api.create_repo(token=hf_token, name=repo_name, repo_type="dataset", private=True)
     repo_id = f"{USER}/{repo_name}"
     hf_api.upload_file(
diff --git a/tests/s3_fixtures.py b/tests/s3_fixtures.py
index 0400f0ea539..44845fe1180 100644
--- a/tests/s3_fixtures.py
+++ b/tests/s3_fixtures.py
@@ -9,7 +9,7 @@
 
 s3_test_bucket_name = "test"
 s3_port = 5555
-s3_endpoint_uri = "http://127.0.0.1:%s/" % s3_port
+s3_endpoint_uri = f"http://127.0.0.1:{s3_port}/"
 
 S3_FAKE_ENV_VARS = {
     "AWS_ACCESS_KEY_ID": "fake_access_key",
@@ -29,7 +29,7 @@ def s3_base():
     old_environ = os.environ.copy()
     os.environ.update(S3_FAKE_ENV_VARS)
 
-    proc = subprocess.Popen(shlex.split("moto_server s3 -p %s" % s3_port))
+    proc = subprocess.Popen(shlex.split(f"moto_server s3 -p {s3_port}"))
 
     timeout = 5
     while timeout > 0:
diff --git a/tests/test_array_xd.py b/tests/test_array_xd.py
index 05caf15858c..e75e8bdb717 100644
--- a/tests/test_array_xd.py
+++ b/tests/test_array_xd.py
@@ -129,7 +129,7 @@ def get_array_feature_types():
     shape_2 = [3, 4, 5, 6, 7]
     return [
         {
-            "testcase_name": "{}d".format(d),
+            "testcase_name": f"{d}d",
             "array_feature": array_feature,
             "shape_1": tuple(shape_1[:d]),
             "shape_2": tuple(shape_2[:d]),
diff --git a/tests/test_builder.py b/tests/test_builder.py
index 71b2c07b35a..a3c119a0c85 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -28,7 +28,7 @@ def _split_generators(self, dl_manager):
         return [SplitGenerator(name=Split.TRAIN)]
 
     def _prepare_split(self, split_generator, **kwargs):
-        fname = "{}-{}.arrow".format(self.name, split_generator.name)
+        fname = f"{self.name}-{split_generator.name}.arrow"
         with ArrowWriter(features=self.info.features, path=os.path.join(self._cache_dir, fname)) as writer:
             writer.write_batch({"text": ["foo"] * 100})
             num_examples, num_bytes = writer.finalize()
@@ -203,7 +203,7 @@ def char_tokenize(example):
             return dataset.map(char_tokenize, cache_file_name=resources_paths["tokenized_dataset"])
 
         def _post_processing_resources(self, split):
-            return {"tokenized_dataset": "tokenized_dataset-{split}.arrow".format(split=split)}
+            return {"tokenized_dataset": f"tokenized_dataset-{split}.arrow"}
 
         with tempfile.TemporaryDirectory() as tmp_dir:
             dummy_builder = DummyBuilder(cache_dir=tmp_dir, name="dummy")
@@ -344,7 +344,7 @@ def _post_process(self, dataset, resources_paths):
                 return dataset
 
         def _post_processing_resources(self, split):
-            return {"index": "Flat-{split}.faiss".format(split=split)}
+            return {"index": f"Flat-{split}.faiss"}
 
         with tempfile.TemporaryDirectory() as tmp_dir:
             dummy_builder = DummyBuilder(cache_dir=tmp_dir, name="dummy")
@@ -412,7 +412,7 @@ def char_tokenize(example):
             return dataset.map(char_tokenize, cache_file_name=resources_paths["tokenized_dataset"])
 
         def _post_processing_resources(self, split):
-            return {"tokenized_dataset": "tokenized_dataset-{split}.arrow".format(split=split)}
+            return {"tokenized_dataset": f"tokenized_dataset-{split}.arrow"}
 
         with tempfile.TemporaryDirectory() as tmp_dir:
             dummy_builder = DummyBuilder(cache_dir=tmp_dir, name="dummy")
@@ -464,7 +464,7 @@ def _post_process(self, dataset, resources_paths):
                 return dataset
 
         def _post_processing_resources(self, split):
-            return {"index": "Flat-{split}.faiss".format(split=split)}
+            return {"index": f"Flat-{split}.faiss"}
 
         with tempfile.TemporaryDirectory() as tmp_dir:
             dummy_builder = DummyBuilder(cache_dir=tmp_dir, name="dummy")
diff --git a/tests/test_dataset_dict.py b/tests/test_dataset_dict.py
index 50b5463c979..d7bfdd7875d 100644
--- a/tests/test_dataset_dict.py
+++ b/tests/test_dataset_dict.py
@@ -30,7 +30,7 @@ def _create_dummy_dataset(self, multiple_columns=False):
             dset = Dataset.from_dict(data)
         else:
             dset = Dataset.from_dict(
-                {"filename": ["my_name-train" + "_" + "{:03d}".format(x) for x in np.arange(30).tolist()]}
+                {"filename": ["my_name-train" + "_" + f"{x:03d}" for x in np.arange(30).tolist()]}
             )
         return dset
 
@@ -320,7 +320,7 @@ def test_sort(self):
             self.assertListEqual(list(dsets.keys()), list(sorted_dsets_1.keys()))
             self.assertListEqual(
                 [f.split("_")[-1] for f in sorted_dsets_1["train"]["filename"]],
-                sorted("{:03d}".format(x) for x in range(30)),
+                sorted("f{x:03d}" for x in range(30)),
             )
 
             indices_cache_file_names = {
@@ -333,7 +333,7 @@ def test_sort(self):
             self.assertListEqual(list(dsets.keys()), list(sorted_dsets_2.keys()))
             self.assertListEqual(
                 [f.split("_")[-1] for f in sorted_dsets_2["train"]["filename"]],
-                sorted(("{:03d}".format(x) for x in range(30)), reverse=True),
+                sorted((f"{x:03d}" for x in range(30)), reverse=True),
             )
             del dsets, sorted_dsets_1, sorted_dsets_2
 
diff --git a/tests/test_streaming_download_manager.py b/tests/test_streaming_download_manager.py
index 32f6fecd6fb..02e292b7685 100644
--- a/tests/test_streaming_download_manager.py
+++ b/tests/test_streaming_download_manager.py
@@ -87,7 +87,7 @@ def __getitem__(self, name):
         for item in self._fs_contents:
             if item["name"] == name:
                 return item
-        raise IndexError("{name} not found!".format(name=name))
+        raise IndexError(f"{name} not found!")
 
     def ls(self, path, detail=True, refresh=True, **kwargs):
         if kwargs.pop("strip_proto", True):
diff --git a/tests/utils.py b/tests/utils.py
index 9ceb5472c48..fee77845e85 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -24,7 +24,7 @@ def parse_flag_from_env(key, default=False):
             _value = strtobool(value)
         except ValueError:
             # More values are supported, but let's keep the message simple.
-            raise ValueError("If set, {} must be yes or no.".format(key))
+            raise ValueError(f"If set, {key} must be yes or no.")
     return _value
 
 


Code review comments:
diff_hunk: @@ -320,7 +320,7 @@ def test_sort(self):
             self.assertListEqual(list(dsets.keys()), list(sorted_dsets_1.keys()))
             self.assertListEqual(
                 [f.split("_")[-1] for f in sorted_dsets_1["train"]["filename"]],
-                sorted("{:03d}".format(x) for x in range(30)),
+                sorted("f{x:03d}" for x in range(30)),
path: tests/test_dataset_dict.py
text: @user1:
```suggestion
                sorted(f"{x:03d}" for x in range(30)),
```