Repository: PyPSA/PyPSA

Issue:
rename import_components_from_dataframe / import_series_from_dataframe
From the users perspective, these functions should be replaced by `.madd` and direct assignment.
Thus we can rename the functions `import_components_from_dataframe` and `import_series_from_dataframe` to `_import_components_from_dataframe` and `_import_series_from_dataframe` (with an underscore in front), in order to make clear that these are back-end functions. 
 

For example, if you have a dataframe `df` which you would use for `import_components_from_dataframe`, use the following syntax:

```python
n.madd(c, df.index, **df)
```
this is shorter and cleaner.

For `import_series_from_dataframe` one has to check whether the index are aligned when 
```
n.pnl(c)[attr] = df
```
also non-string type columns of `df` should be converted to strings. 
import_components_from_dataframe ignores default values
<!-- Please do not post usage questions here. Ask them on the PyPSA mailing list: https://groups.google.com/forum/#!forum/pypsa -->

## Checklist

- [ ] I am using the latest version of PyPSA available on the 4th of October 2023, without any modification.

## Describe the Bug

If some parameters in the dataframe are Nan, the `import_components_from_dataframe` does not take into consideration the default values. For example, if the `p_max_pu` and `p_min_pu` of Generators are not defined in an inputted data frame, the network does not consider the default values of the component parameters and `consistency_check()` shows them as warnings. This finally results in an infeasible solution.

n.add and n.madd different behaviour
## Align `n.add` and `n.madd`

When adding a component with non-default attributes, `n.add`  would not add the attribute while `n.madd`  does, for example
`n.add("Bus", "co2 atmosphere", location="EU", carrier="co2", unit="t_co2")`
Does not add the attribute `location`, since it is not defined by default while
`n.madd("Bus", ["co2 atmosphere"], location=["EU"], carrier="co2", unit="t_co2")`
add the column location to the dataframe.
Refactor: Generalize add component functions
What is the reason for having both `n.add` and `n.madd`? Right now, there are two versions with duplicate structures that also don't have the exact same behavior, which leads to bugs (see #867). This is hard to maintain, not a clean API and I don't see the reason for that or am I missing something?

We could generalize both and deprecate `n.madd`, that would be the cleanest solution and a consistent API. But a big interference in the API, but currently I don't think it breaks anything. I'll add a Draft PR below, which already has a uniform n.add function. All tests run through, but this is a draft.

If both should be kept for some reason, at least the redundancy can be resolved so that both functions only serve as wrappers.

What do you think @fneum @FabianHofmann ? Take a look at the PR 




Discussion:
I'd like that!

Thanks for letting us know. In the meantime, you can do:

```py
n.madd(c, df.index, **df)
```

https://github.com/PyPSA/PyPSA/issues/341
Just to add on to this, `import_components_from_dataframe` is used internally in PyPSA too in ways that have been causing me some trouble. In particular, when clustering networks, the new (output) variable `n_mod` receives NaN values at [clustering/spatial.py#L262](https://github.com/PyPSA/PyPSA/blob/master/pypsa/clustering/spatial.py#L262) (in `aggregateoneport`), and these NaN values later can lead to problems in `get_clustering_from_busmap` where a call to `import_components_from_dataframe` now raises an error because NaN values cannot be cast to `int`.

It seems like either PyPSA internally must be able to accept NaN values even in columns that nominally should be cast to `int`, or that `aggregateoneport` (and maybe other functions?) need to be fixed so as not lot let NaN values slip into `int`-typed columns.

(This problem seems to make PyPSA-Eur incompatible with PyPSA 0.26, at least for me.)

i ran into the same issue today, see https://github.com/PyPSA/pypsa-de/issues/9





Patch:
diff --git a/doc/api/networks.rst b/doc/api/networks.rst
index f8f1455a4..552c28dc1 100644
--- a/doc/api/networks.rst
+++ b/doc/api/networks.rst
@@ -23,7 +23,9 @@ General methods
     ~pypsa.Network.set_snapshots
     ~pypsa.Network.set_investment_periods
     ~pypsa.Network.add
+    ~pypsa.Network.madd
     ~pypsa.Network.remove
+    ~pypsa.Network.mremove
     ~pypsa.Network.copy
     ~pypsa.Network.branches
     ~pypsa.Network.passive_branches
diff --git a/doc/getting-started/quick-start.rst b/doc/getting-started/quick-start.rst
index e18d07974..c5c3127a4 100644
--- a/doc/getting-started/quick-start.rst
+++ b/doc/getting-started/quick-start.rst
@@ -13,7 +13,7 @@ In principle, PyPSA networks can be assigned with
 
     network = pypsa.Network()
 
-Components like buses can be added with :py:meth:`pypsa.Network.add` or :py:meth:`pypsa.Network.madd`
+Components like buses can be added with :py:meth:`pypsa.Network.add`.
 
 .. code:: python
 
diff --git a/doc/references/release-notes.rst b/doc/references/release-notes.rst
index 13f4ea731..187eca4c3 100644
--- a/doc/references/release-notes.rst
+++ b/doc/references/release-notes.rst
@@ -5,11 +5,19 @@ Release Notes
 .. Upcoming Release
 .. ================
 
-.. .. warning:: 
+.. warning:: 
   
-..   The features listed below are not released yet, but will be part of the next release! 
-..   To use the features already you have to install the ``master`` branch, e.g. 
-..   ``pip install git+https://github.com/pypsa/pypsa#egg=pypsa``.
+  The features listed below are not released yet, but will be part of the next release! 
+  To use the features already you have to install the ``master`` branch, e.g. 
+  ``pip install git+https://github.com/pypsa/pypsa#egg=pypsa``.
+
+* :meth:`n.add <pypsa.Network.add>` now handles the addition of a single or multiple
+  components, has more robust index alignment checks allows to overwrite existing
+  components using the new argument ``overwrite``. (https://github.com/PyPSA/PyPSA/pull/896)
+
+* Deprecate :meth:`n.madd <pypsa.Network.madd>` and :meth:`n.mremove <pypsa.Network.mremove>`. 
+  :meth:`n.add <pypsa.Network.add>` and :meth:`n.remove <pypsa.Network.remove>` are now used for 
+  adding/ removing multiple as well as single components. (https://github.com/PyPSA/PyPSA/pull/896)
 
 
 * A new attribute for one-port and branch components `active` was added. If set to true (default), the asset is considered active for all functionality, including optimization and power flow calculation. If set to false, the asset is considered inactive and is excluded from the optimization, power flow and statistics modules. The active attribute can be thought of as a global filter on the components. When running a multi-horizon optimization, the active attribute is considered a global condition for each horizon. Then assets are considered active only if `active` is true and the investment period falls within the lifetime of the asset.
diff --git a/doc/user-guide/import-export.rst b/doc/user-guide/import-export.rst
index a9d5d561f..ac602f444 100644
--- a/doc/user-guide/import-export.rst
+++ b/doc/user-guide/import-export.rst
@@ -9,9 +9,8 @@ There is limited functionality to import from Pypower and Pandapower.
 Adding components
 =================
 
-Networks can also be built step-by-step for each component by calling :py:meth:`pypsa.Network.add`. 
-
-Multiple components can be added by calling :py:meth:`pypsa.Network.madd`.
+Networks can also be built step-by-step for each component by calling 
+:py:meth:`pypsa.Network.add` to add a single or multiple components.
 
 To add multiple components whose static attributes are given in a
 pandas DataFrame, use :py:meth:`pypsa.Network.import_components_from_dataframe`
diff --git a/examples/notebooks/capacity-expansion-planning-single-node.ipynb b/examples/notebooks/capacity-expansion-planning-single-node.ipynb
index 0270e8289..c653d8ba9 100644
--- a/examples/notebooks/capacity-expansion-planning-single-node.ipynb
+++ b/examples/notebooks/capacity-expansion-planning-single-node.ipynb
@@ -529,7 +529,7 @@
     "    \"battery storage\",\n",
     "]\n",
     "\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Carrier\",\n",
     "    carriers,\n",
     "    color=[\"dodgerblue\", \"aquamarine\", \"gold\", \"indianred\", \"magenta\", \"yellowgreen\"],\n",
diff --git a/examples/notebooks/optimization-with-linopy-migrate-extra-functionalities.ipynb b/examples/notebooks/optimization-with-linopy-migrate-extra-functionalities.ipynb
index 073661de0..455e18afe 100644
--- a/examples/notebooks/optimization-with-linopy-migrate-extra-functionalities.ipynb
+++ b/examples/notebooks/optimization-with-linopy-migrate-extra-functionalities.ipynb
@@ -84,7 +84,7 @@
     "n.storage_units_t.state_of_charge_set.loc[n.snapshots[7], \"su\"] = 100\n",
     "\n",
     "n.add(\"Bus\", \"storebus\", carrier=\"hydro\", x=-5, y=55)\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Link\",\n",
     "    [\"battery_power\", \"battery_discharge\"],\n",
     "    \"\",\n",
@@ -95,7 +95,7 @@
     "    p_nom_extendable=True,\n",
     "    p_nom_max=1000,\n",
     ")\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Store\",\n",
     "    [\"store\"],\n",
     "    bus=\"storebus\",\n",
diff --git a/examples/notebooks/optimization-with-linopy.ipynb b/examples/notebooks/optimization-with-linopy.ipynb
index 033e8e00a..de6253663 100644
--- a/examples/notebooks/optimization-with-linopy.ipynb
+++ b/examples/notebooks/optimization-with-linopy.ipynb
@@ -140,7 +140,7 @@
    "outputs": [],
    "source": [
     "n.add(\"Bus\", \"storebus\", carrier=\"hydro\", x=-5, y=55)\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Link\",\n",
     "    [\"battery_power\", \"battery_discharge\"],\n",
     "    \"\",\n",
@@ -151,7 +151,7 @@
     "    p_nom_extendable=True,\n",
     "    p_nom_max=1000,\n",
     ")\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Store\",\n",
     "    [\"store\"],\n",
     "    bus=\"storebus\",\n",
diff --git a/examples/notebooks/scigrid-redispatch.ipynb b/examples/notebooks/scigrid-redispatch.ipynb
index bec382241..459c38604 100644
--- a/examples/notebooks/scigrid-redispatch.ipynb
+++ b/examples/notebooks/scigrid-redispatch.ipynb
@@ -151,10 +151,10 @@
     "    c.df.bus0 = c.df.bus0.map(zones)\n",
     "    c.df.bus1 = c.df.bus1.map(zones)\n",
     "    internal = c.df.bus0 == c.df.bus1\n",
-    "    m.mremove(c.name, c.df.loc[internal].index)\n",
+    "    m.remove(c.name, c.df.loc[internal].index)\n",
     "\n",
-    "m.mremove(\"Bus\", m.buses.index)\n",
-    "m.madd(\"Bus\", [\"North\", \"South\"]);"
+    "m.remove(\"Bus\", m.buses.index)\n",
+    "m.add(\"Bus\", [\"North\", \"South\"]);"
    ]
   },
   {
@@ -258,9 +258,9 @@
     "up.columns = up.columns.map(lambda x: x + \" ramp up\")\n",
     "down.columns = down.columns.map(lambda x: x + \" ramp down\")\n",
     "\n",
-    "n.madd(\"Generator\", g_up.index, p_max_pu=up, **g_up.drop(\"p_max_pu\", axis=1))\n",
+    "n.add(\"Generator\", g_up.index, p_max_pu=up, **g_up.drop(\"p_max_pu\", axis=1))\n",
     "\n",
-    "n.madd(\n",
+    "n.add(\n",
     "    \"Generator\",\n",
     "    g_down.index,\n",
     "    p_min_pu=down,\n",
diff --git a/pypsa/clustering/spatial.py b/pypsa/clustering/spatial.py
index 6b2976d34..cc92eec74 100644
--- a/pypsa/clustering/spatial.py
+++ b/pypsa/clustering/spatial.py
@@ -485,8 +485,8 @@ def get_clustering_from_busmap(
 
     clustered = Network()
 
-    io.import_components_from_dataframe(clustered, buses, "Bus")
-    io.import_components_from_dataframe(clustered, lines, "Line")
+    io._import_components_from_dataframe(clustered, buses, "Bus")
+    io._import_components_from_dataframe(clustered, lines, "Line")
 
     # Carry forward global constraints to clustered n.
     clustered.global_constraints = n.global_constraints
@@ -501,7 +501,7 @@ def get_clustering_from_busmap(
             )
         for attr, df in lines_t.items():
             if not df.empty:
-                io.import_series_from_dataframe(clustered, df, "Line", attr)
+                io._import_series_from_dataframe(clustered, df, "Line", attr)
 
     one_port_components = n.one_port_components.copy()
 
@@ -517,11 +517,11 @@ def get_clustering_from_busmap(
             with_time=with_time,
             custom_strategies=generator_strategies,
         )
-        io.import_components_from_dataframe(clustered, generators, "Generator")
+        io._import_components_from_dataframe(clustered, generators, "Generator")
         if with_time:
             for attr, df in generators_pnl.items():
                 if not df.empty:
-                    io.import_series_from_dataframe(clustered, df, "Generator", attr)
+                    io._import_series_from_dataframe(clustered, df, "Generator", attr)
 
     for one_port in aggregate_one_ports:
         one_port_components.remove(one_port)
@@ -532,14 +532,14 @@ def get_clustering_from_busmap(
             with_time=with_time,
             custom_strategies=one_port_strategies.get(one_port, {}),
         )
-        io.import_components_from_dataframe(clustered, new_df, one_port)
+        io._import_components_from_dataframe(clustered, new_df, one_port)
         for attr, df in new_pnl.items():
-            io.import_series_from_dataframe(clustered, df, one_port, attr)
+            io._import_series_from_dataframe(clustered, df, one_port, attr)
 
     # Collect remaining one ports
 
     for c in n.iterate_components(one_port_components):
-        io.import_components_from_dataframe(
+        io._import_components_from_dataframe(
             clustered,
             c.df.assign(bus=c.df.bus.map(busmap)).dropna(subset=["bus"]),
             c.name,
@@ -549,7 +549,7 @@ def get_clustering_from_busmap(
         for c in n.iterate_components(one_port_components):
             for attr, df in c.pnl.items():
                 if not df.empty:
-                    io.import_series_from_dataframe(clustered, df, c.name, attr)
+                    io._import_series_from_dataframe(clustered, df, c.name, attr)
 
     new_links = (
         n.links.assign(bus0=n.links.bus0.map(busmap), bus1=n.links.bus1.map(busmap))
@@ -569,14 +569,14 @@ def get_clustering_from_busmap(
     if scale_link_capital_costs:
         new_links["capital_cost"] *= (new_links.length / n.links.length).fillna(1)
 
-    io.import_components_from_dataframe(clustered, new_links, "Link")
+    io._import_components_from_dataframe(clustered, new_links, "Link")
 
     if with_time:
         for attr, df in n.links_t.items():
             if not df.empty:
-                io.import_series_from_dataframe(clustered, df, "Link", attr)
+                io._import_series_from_dataframe(clustered, df, "Link", attr)
 
-    io.import_components_from_dataframe(clustered, n.carriers, "Carrier")
+    io._import_components_from_dataframe(clustered, n.carriers, "Carrier")
 
     clustered.determine_network_topology()
 
diff --git a/pypsa/components.py b/pypsa/components.py
index 402aad2c1..222e0b286 100644
--- a/pypsa/components.py
+++ b/pypsa/components.py
@@ -20,6 +20,7 @@
 import pandas as pd
 import pyproj
 import validators
+from deprecation import deprecated
 from pyproj import CRS, Transformer
 from scipy.sparse import csgraph
 
@@ -49,10 +50,11 @@
     get_extendable_i,
     get_non_extendable_i,
     get_switchable_as_dense,
-    update_linkports_component_attrs,
 )
 from pypsa.graph import adjacency_matrix, graph, incidence_matrix
 from pypsa.io import (
+    _import_components_from_dataframe,
+    _import_series_from_dataframe,
     export_to_csv_folder,
     export_to_hdf5,
     export_to_netcdf,
@@ -81,6 +83,7 @@
 )
 from pypsa.plot import explore, iplot, plot  # type: ignore
 from pypsa.statistics import StatisticsAccessor
+from pypsa.types import is_1d_list_like
 from pypsa.utils import as_index
 
 if TYPE_CHECKING:
@@ -223,9 +226,11 @@ class Network:
     export_to_netcdf = export_to_netcdf
     import_from_pypower_ppc = import_from_pypower_ppc
     import_from_pandapower_net = import_from_pandapower_net
-    import_components_from_dataframe = import_components_from_dataframe
     merge = merge
-    import_series_from_dataframe = import_series_from_dataframe
+    _import_components_from_dataframe = _import_components_from_dataframe
+    import_components_from_dataframe = import_components_from_dataframe  # Deprecated
+    _import_series_from_dataframe = _import_series_from_dataframe
+    import_series_from_dataframe = import_series_from_dataframe  # Deprecated
 
     # from pypsa.pf
     calculate_dependent_values = calculate_dependent_values
@@ -507,7 +512,7 @@ def read_in_default_standard_types(self) -> None:
                 file_name, index_col=0
             )
 
-            self.import_components_from_dataframe(
+            self._import_components_from_dataframe(
                 self.components[std_type]["standard_types"], std_type
             )
 
@@ -612,7 +617,7 @@ def set_snapshots(
         of type `pd.DatetimeIndex`.
 
         This will reindex all components time-dependent DataFrames
-        (:py:meth:`pypsa.network.pnl`). NaNs are filled with the default value for that quantity.
+        (:py:meth:`pypsa.Network.pnl`). NaNs are filled with the default value for that quantity.
 
         Parameters
         ----------
@@ -821,130 +826,264 @@ def investment_period_weightings(self, df: pd.DataFrame) -> None:
             )
         self._investment_period_weightings = df
 
-    def add(self, class_name: str, name: str, **kwargs: Any) -> None:
+    def add(
+        self,
+        class_name: str,
+        name: str | int | Sequence[int | str],
+        suffix: str = "",
+        overwrite: bool = False,
+        **kwargs: Any,
+    ) -> pd.Index:
         """
-        Add a single component to the network.
+        Add components to the network.
+
+        Handles addition of single and multiple components along with their attributes.
+        Pass a list of names to add multiple components at once or pass a single name
+        to add a single component.
 
-        Adds it to component DataFrame.
+        When a single component is added, all non-scalar attributes are assumed to be
+        time-varying and indexed by snapshots. When multiple components are added, all
+        non-scalar attributes are assumed to be static and indexed by names. If you want
+        to add time-varying attributes to multiple components, you can pass a 2D array/
+        DataFrame where the first dimension is snapshots and the second dimension is
+        names.
 
         Any attributes which are not specified will be given the default
         value from :doc:`/user-guide/components`.
 
-        This method is slow for many components; instead use ``madd`` or
-        ``import_components_from_dataframe`` (see below).
-
         Parameters
         ----------
-        class_name : string
+        class_name : str
             Component class name in ("Bus", "Generator", "Load", "StorageUnit",
             "Store", "ShuntImpedance", "Line", "Transformer", "Link").
-        name : string
-            Component name
-        kwargs
-            Component attributes, e.g. x=0.1, length=123
+        name : str or int or list of str or list of int
+            Component name(s)
+        suffix : str, default ""
+            All components are named after name with this added suffix.
+        overwrite : bool, default False
+            If True, existing components with the same names as in `name` will be
+            overwritten. Otherwise only new components will be added and others will be
+            ignored.
+        kwargs : Any
+            Component attributes, e.g. x=[0.1, 0.2], can be list, pandas.Series
+            of pandas.DataFrame for time-varying
+
+        Returns
+        -------
+        new_names : pandas.index
+            Names of new components (including suffix)
 
         Examples
         --------
-        >>> network.add("Bus", "my_bus_0")
-        >>> network.add("Bus", "my_bus_1", v_nom=380)
-        >>> network.add("Line", "my_line_name", bus0="my_bus_0", bus1="my_bus_1", length=34, r=2, x=4)
-
-        """
-        if class_name not in self.components:
-            msg = f"Component class {class_name} not found"
-            raise ValueError(msg)
+        Add a single component:
 
-        cls_df = self.df(class_name)
-        cls_pnl = self.pnl(class_name)
+        >>> n.add("Bus", "my_bus_0")
+        >>> n.add("Bus", "my_bus_1", v_nom=380)
+        >>> n.add("Line", "my_line_name", bus0="my_bus_0", bus1="my_bus_1", length=34, r=2, x=4)
 
-        name = str(name)
+        Add multiple components with static attributes:
 
-        if name in cls_df.index:
-            msg = (
-                f"Failed to add {class_name} component {name} because there is "
-                f"already an object with this name in "
-                f"{self.components[class_name]['list_name']}"
-            )
-            raise ValueError(msg)
+        >>> n.add("Load", ["load 1", "load 2"],
+        ...       bus=["1", "2"],
+        ...       p_set=np.random.rand(len(network.snapshots), 2))
 
-        if class_name == "Link":
-            update_linkports_component_attrs(self, kwargs.keys())
+        Add multiple components with time-varying attributes:
 
-        attrs = self.components[class_name]["attrs"]
-
-        static_attrs = attrs[attrs.static].drop("name")
+        >>> import pandas as pd, numpy as np
+        >>> buses = range(13)
+        >>> snapshots = range(7)
+        >>> n = pypsa.Network()
+        >>> n.set_snapshots(snapshots)
+        >>> n.add("Bus", buses)
+        >>> # add load as numpy array
+        >>> n.add("Load",
+        ...       n.buses.index + " load",
+        ...       bus=buses,
+        ...       p_set=np.random.rand(len(snapshots), len(buses)))
+        >>> # add wind availability as pandas DataFrame
+        >>> wind = pd.DataFrame(np.random.rand(len(snapshots), len(buses)),
+        ...        index=n.snapshots,
+        ...        columns=buses)
+        >>> #use a suffix to avoid boilerplate to rename everything
+        >>> n.add("Generator",
+        ...       buses,
+        ...       suffix=' wind',
+        ...       bus=buses,
+        ...       p_nom_extendable=True,
+        ...       capital_cost=1e5,
+        ...       p_max_pu=wind)
 
-        # This guarantees that the correct attribute type is maintained
-        obj_df = pd.DataFrame(
-            data=[static_attrs.default], index=[name], columns=static_attrs.index
-        )
+        """
+        if class_name not in self.components:
+            msg = f"Component class {class_name} not found."
+            raise ValueError(msg)
+        # Process name/names to pandas.Index of strings and add suffix
+        single_component = np.isscalar(name)
+        names = pd.Index([name]) if single_component else pd.Index(name)
+        names = names.astype(str) + suffix
 
-        # Remove all NaN columns before concatenating
-        obj_df = obj_df.dropna(axis=1, how="all")
-        new_df = pd.concat([cls_df, obj_df], sort=False)
+        names_str = "name" if single_component else "names"
+        # Read kwargs into static and time-varying attributes
+        series = {}
+        static = {}
 
-        new_df.index.name = class_name
-        setattr(self, self.components[class_name]["list_name"], new_df)
+        # Check if names are unique
+        if not names.is_unique:
+            msg = f"Names for {class_name} must be unique."
+            raise ValueError(msg)
 
         for k, v in kwargs.items():
-            if k not in attrs.index:
-                logger.warning(
-                    f"{class_name} has no attribute {k}, " "ignoring this passed value."
+            # If index/ columnes are passed (pd.DataFrame or pd.Series)
+            # - cast names index to string and add suffix
+            # - check if passed index/ columns align
+            msg = "{} has an index which does not align with the passed {}."
+            if isinstance(v, pd.Series) and single_component:
+                if not v.index.equals(self.snapshots):
+                    raise ValueError(msg.format(f"Series {k}", "network snapshots"))
+            elif isinstance(v, pd.Series):
+                # Cast names index to string + suffix
+                v = v.rename(
+                    index=lambda s: str(s) if str(s).endswith(suffix) else s + suffix
                 )
-                continue
-            typ = attrs.at[k, "typ"]
-            if not attrs.at[k, "varying"]:
-                new_df.at[name, k] = typ(v) if typ != "geometry" else v
-            elif attrs.at[k, "static"] and not isinstance(
-                v, (pd.Series, pd.DataFrame, np.ndarray, list)
-            ):
-                new_df.at[name, k] = typ(v)
-            else:
-                cls_pnl[k][name] = pd.Series(data=v, index=self.snapshots, dtype=typ)
+                if not v.index.equals(names):
+                    raise ValueError(msg.format(f"Series {k}", names_str))
+            if isinstance(v, pd.DataFrame):
+                # Cast names columns to string + suffix
+                v = v.rename(
+                    columns=lambda s: str(s) if str(s).endswith(suffix) else s + suffix
+                )
+                if not v.index.equals(self.snapshots):
+                    raise ValueError(msg.format(f"DataFrame {k}", "network snapshots"))
+                if not v.columns.equals(names):
+                    raise ValueError(msg.format(f"DataFrame {k}", names_str))
+
+            # Convert list-like and 1-dim array to pandas.Series
+            if is_1d_list_like(v):
+                try:
+                    if single_component:
+                        v = pd.Series(v, index=self.snapshots)
+                    else:
+                        v = pd.Series(v)
+                        if len(v) == 1:
+                            v = v.iloc[0]
+                            logger.warning(
+                                f"Single value sequence for {k} is treated as a scalar "
+                                f"and broadcasted to all components. It is recommended "
+                                f"to explicitly pass a scalar instead."
+                            )
+                        else:
+                            v.index = names
+                except ValueError:
+                    expec_str = (
+                        f"{len(self.snapshots)} for each snapshot."
+                        if single_component
+                        else f"{len(names)} for each component name."
+                    )
+                    msg = f"Data for {k} has length {len(v)} but expected {expec_str}"
+                    raise ValueError(msg)
+            # Convert 2-dim array to pandas.DataFrame
+            if isinstance(v, np.ndarray):
+                if v.shape == (len(self.snapshots), len(names)):
+                    v = pd.DataFrame(v, index=self.snapshots, columns=names)
+                else:
+                    msg = (
+                        f"Array {k} has shape {v.shape} but expected "
+                        f"({len(self.snapshots)}, {len(names)})."
+                    )
+                    raise ValueError(msg)
 
-        for attr in [attr for attr in new_df if attr.startswith("bus")]:
-            bus_name = new_df.at[name, attr]
-            # allow empty buses for multi-ports
-            port = int(attr[-1]) if attr[-1].isdigit() else 0
-            if bus_name not in self.buses.index and not (bus_name == "" and port > 1):
-                logger.warning(
-                    f"The bus name `{bus_name}` given for {attr} "
-                    f"of {class_name} `{name}` does not appear "
-                    "in network.buses"
+            if isinstance(v, dict):
+                msg = (
+                    "Dictionaries are not supported as attribute values. Please use "
+                    "pandas.Series or pandas.DataFrame instead."
                 )
+                raise NotImplementedError(msg)
+
+            # Handle addition of single component
+            if single_component:
+                # Read 1-dim data as time-varying attribute
+                if isinstance(v, pd.Series):
+                    series[k] = pd.DataFrame(
+                        v.values, index=self.snapshots, columns=names
+                    )
+                # Read 0-dim data as static attribute
+                else:
+                    static[k] = v
+
+            # Handle addition of multiple components
+            elif not single_component:
+                # Read 2-dim data as time-varying attribute
+                if isinstance(v, pd.DataFrame):
+                    series[k] = v
+                # Read 1-dim data as static attribute
+                elif isinstance(v, pd.Series):
+                    static[k] = v.values
+                # Read scalar data as static attribute
+                else:
+                    static[k] = v
+
+        # Load static attributes as components
+        if static:
+            static_df = pd.DataFrame(static, index=names)
+        else:
+            static_df = pd.DataFrame(index=names)
+        self._import_components_from_dataframe(
+            static_df, class_name, overwrite=overwrite
+        )
+
+        # Load time-varying attributes as components
+        for k, v in series.items():
+            self._import_series_from_dataframe(v, class_name, k, overwrite=overwrite)
+
+        return names
 
-    def remove(self, class_name: str, name: str) -> None:
+    def remove(
+        self,
+        class_name: str,
+        name: str | int | Sequence[int | str],
+        suffix: str = "",
+    ) -> None:
         """
-        Removes a single component from the network.
+        Removes a single component or a list of components from the network.
 
         Removes it from component DataFrames.
 
         Parameters
         ----------
-        class_name : string
+        class_name : str
             Component class name
-        name : string
-            Component name
+        name : str, int, list-like or pandas.Index
+            Component name(s)
+        suffix : str, default ''
+
 
         Examples
         --------
-        >>> network.remove("Line", "my_line 12345")
-
+        >>> n.remove("Line", "my_line 12345")
+        >>> n.remove("Line", ["line x", "line y"])
         """
         if class_name not in self.components:
-            logger.error(f"Component class {class_name} not found")
-            return None
+            msg = f"Component class {class_name} not found"
+            raise ValueError(msg)
 
-        cls_df = self.df(class_name)
+        # Process name/names to pandas.Index of strings and add suffix
+        names = pd.Index([name]) if np.isscalar(name) else pd.Index(name)
+        names = names.astype(str) + suffix
 
-        cls_df.drop(name, inplace=True)
+        # Drop from static components
+        cls_df = self.df(class_name)
+        cls_df.drop(names, inplace=True)
 
+        # Drop from time-varying components
         pnl = self.pnl(class_name)
-
         for df in pnl.values():
-            if name in df:
-                df.drop(name, axis=1, inplace=True)
+            df.drop(df.columns.intersection(names), axis=1, inplace=True)
 
+    @deprecated(
+        deprecated_in="0.31",
+        removed_in="1.0",
+        details="Use `n.add` as a drop-in replacement instead.",
+    )
     def madd(
         self,
         class_name: str,
@@ -955,6 +1094,10 @@ def madd(
         """
         Add multiple components to the network, along with their attributes.
 
+        ``network.madd`` is deprecated and will be removed in version 1.0. Use
+        :py:meth:`pypsa.Network.add` instead. It can handle both single and multiple
+        addition of components.
+
         Make sure when adding static attributes as pandas Series that they are indexed
         by names. Make sure when adding time-varying attributes as pandas DataFrames that
         their index is a superset of network.snapshots and their columns are a
@@ -1017,43 +1160,21 @@ def madd(
         ...        p_max_pu=wind)
 
         """
-        if class_name not in self.components:
-            logger.error(f"Component class {class_name} not found")
-            return None
-
-        if not isinstance(names, pd.Index):
-            names = pd.Index(names)
-
-        new_names = names.astype(str) + suffix
-
-        static = {}
-        series = {}
-        for k, v in kwargs.items():
-            if isinstance(v, pd.DataFrame):
-                series[k] = v.rename(columns=lambda i: str(i) + suffix)
-            elif isinstance(v, pd.Series):
-                static[k] = v.rename(lambda i: str(i) + suffix)
-            elif isinstance(v, np.ndarray) and v.shape == (
-                len(self.snapshots),
-                len(names),
-            ):
-                series[k] = pd.DataFrame(v, index=self.snapshots, columns=new_names)
-            else:
-                static[k] = v
-
-        self.import_components_from_dataframe(
-            pd.DataFrame(static, index=new_names), class_name
-        )
-
-        for k, v in series.items():
-            self.import_series_from_dataframe(v, class_name, k)
-
-        return new_names
+        return self.add(class_name=class_name, name=names, suffix=suffix, **kwargs)
 
+    @deprecated(
+        deprecated_in="0.31",
+        removed_in="1.0",
+        details="Use `n.remove` as a drop-in replacement instead.",
+    )
     def mremove(self, class_name: str, names: Sequence) -> None:
         """
         Removes multiple components from the network.
 
+        ``network.mremove`` is deprecated and will be removed in version 1.0. Use
+        py:meth:`pypsa.Network.remove` instead. It can handle both single and multiple removal of
+        components.
+
         Removes them from component DataFrames.
 
         Parameters
@@ -1068,21 +1189,7 @@ def mremove(self, class_name: str, names: Sequence) -> None:
         >>> network.mremove("Line", ["line x", "line y"])
 
         """
-        if class_name not in self.components:
-            logger.error(f"Component class {class_name} not found")
-            return None
-
-        if not isinstance(names, pd.Index):
-            names = pd.Index(names)
-
-        cls_df = self.df(class_name)
-
-        cls_df.drop(names, inplace=True)
-
-        pnl = self.pnl(class_name)
-
-        for df in pnl.values():
-            df.drop(df.columns.intersection(names), axis=1, inplace=True)
+        self.remove(class_name=class_name, name=names)
 
     def _retrieve_overridden_components(self) -> tuple[pd.DataFrame, Dict]:
         components_index = list(self.components.keys())
@@ -1194,7 +1301,7 @@ def copy(
             else:
                 df = component.df
 
-            import_components_from_dataframe(network, df, component.name)
+            _import_components_from_dataframe(network, df, component.name)
 
         # Copy time-varying data, if given
 
@@ -1277,7 +1384,7 @@ def __getitem__(self, key: str) -> Network:
             override_components=override_components,
             override_component_attrs=override_component_attrs,
         )
-        n.import_components_from_dataframe(
+        n._import_components_from_dataframe(
             pd.DataFrame(self.buses.loc[key]).assign(sub_network=""), "Bus"
         )
         buses_i = n.buses.index
@@ -1289,21 +1396,21 @@ def __getitem__(self, key: str) -> Network:
             - self.branch_components
         )
         for c in rest_components - {"Bus", "SubNetwork"}:
-            n.import_components_from_dataframe(pd.DataFrame(self.df(c)), c)
+            n._import_components_from_dataframe(pd.DataFrame(self.df(c)), c)
 
         for c in self.standard_type_components:
             df = self.df(c).drop(self.components[c]["standard_types"].index)
-            n.import_components_from_dataframe(pd.DataFrame(df), c)
+            n._import_components_from_dataframe(pd.DataFrame(df), c)
 
         for c in self.one_port_components:
             df = self.df(c).loc[lambda df: df.bus.isin(buses_i)]
-            n.import_components_from_dataframe(pd.DataFrame(df), c)
+            n._import_components_from_dataframe(pd.DataFrame(df), c)
 
         for c in self.branch_components:
             df = self.df(c).loc[
                 lambda df: df.bus0.isin(buses_i) & df.bus1.isin(buses_i)
             ]
-            n.import_components_from_dataframe(pd.DataFrame(df), c)
+            n._import_components_from_dataframe(pd.DataFrame(df), c)
 
         n.set_snapshots(self.snapshots[time_i])
         for c in self.all_components:
diff --git a/pypsa/consistency.py b/pypsa/consistency.py
index 01ef73770..b2ecc66c9 100644
--- a/pypsa/consistency.py
+++ b/pypsa/consistency.py
@@ -41,9 +41,8 @@ def check_for_unknown_buses(network: Network, component: Component) -> None:
             missing &= component.df[attr] != ""
         if missing.any():
             logger.warning(
-                "The following %s have %s which are not defined:\n%s",
+                "The following %s have buses which are not defined:\n%s",
                 component.list_name,
-                attr,
                 component.df.index[missing],
             )
 
diff --git a/pypsa/descriptors.py b/pypsa/descriptors.py
index 0196ffe17..433a38a0f 100644
--- a/pypsa/descriptors.py
+++ b/pypsa/descriptors.py
@@ -387,9 +387,29 @@ def get_bounds_pu(
         return min_pu.reindex(columns=index), max_pu.reindex(columns=index)
 
 
-def update_linkports_doc_changes(
-    s: str | Collection[str], i: int, j: str
-) -> str | Collection[str]:
+def update_linkports_doc_changes(s: Any, i: int, j: str) -> Any:
+    """
+    Update components documentation for link ports.
+
+    Multi-linkports require the following changes:
+    1. Replaces every occurrence of the substring `j` with `i`.
+    2. Make attribute required
+
+    Parameters
+    ----------
+    s : An
+        String to update.
+    i : int
+        Integer to replace `j` with.
+    j : string
+        Substring to replace.
+
+    Returns
+    -------
+    Any : Updated string or original value if not a string.
+
+    """
+
     if not isinstance(s, str) or len(s) == 1:
         return s
     return s.replace(j, str(i)).replace("required", "optional")
@@ -398,25 +418,38 @@ def update_linkports_doc_changes(
 def update_linkports_component_attrs(
     n: Network, where: Iterable[str] | None = None
 ) -> None:
+    """
+    Update the Link components attributes to add the additional ports.
+
+    Parameters
+    ----------
+    n : Network
+        Network instance to which additional ports will be added.
+    where : Iterable[str] or None, optional
+
+        Filters for specific subsets of data by providing an iterable of tags
+        or identifiers. If None, no filtering is applied and additional link
+        ports are considered for all connectors.
+    """
+
     ports = additional_linkports(n, where)
+    ports.sort(reverse=True)
     c = "Link"
 
     for i, attr in product(ports, ["bus", "efficiency", "p"]):
         target = f"{attr}{i}"
         if target in n.components[c]["attrs"].index:
             continue
-        to_replace = "1"
         j = "1" if attr != "efficiency" else ""
+        base_attr = attr + j
+        base_attr_index = n.components[c]["attrs"].index.get_loc(base_attr)
+        n.components[c]["attrs"].index.insert(base_attr_index + 1, target)
         n.components[c]["attrs"].loc[target] = (
             n.components[c]["attrs"]
             .loc[attr + j]
-            .apply(update_linkports_doc_changes, args=(to_replace, i))
-        )
-        n.components[c]["attrs"].loc[target] = (
-            n.components[c]["attrs"]
-            .loc[attr + j]
-            .apply(update_linkports_doc_changes, args=(to_replace, i))
+            .apply(update_linkports_doc_changes, args=("1", i))
         )
+        # Also update container for varying attributes
         if attr in ["efficiency", "p"] and target not in n.pnl(c).keys():
             df = pd.DataFrame(index=n.snapshots, columns=[], dtype=float)
             df.index.name = "snapshot"
@@ -427,6 +460,20 @@ def update_linkports_component_attrs(
 
 
 def additional_linkports(n: Network, where: Iterable[str] | None = None) -> list[str]:
+    """
+    Identify additional link ports (bus connections) beyond predefined ones.
+
+    Parameters
+    ----------
+    n : pypsa.Network
+    where : iterable of strings, default None
+        Subset of columns to consider. Takes link columns by default.
+
+    Returns
+    -------
+    list of strings
+        List of additional link ports. E.g. ["2", "3"] for bus2, bus3.
+    """
     if where is None:
         where = n.links.columns
     return [i[3:] for i in where if i.startswith("bus") and i not in ["bus0", "bus1"]]
diff --git a/pypsa/examples.py b/pypsa/examples.py
index 9ea4097e9..9cdab2aa5 100644
--- a/pypsa/examples.py
+++ b/pypsa/examples.py
@@ -144,5 +144,5 @@ def scigrid_de(update: bool = False, from_master: bool = False) -> Network:
             for carrier in c.df.carrier.unique()
         }
     )
-    n.madd("Carrier", carriers)
+    n.add("Carrier", carriers)
     return n
diff --git a/pypsa/io.py b/pypsa/io.py
index f84f38df9..9569e6e5e 100644
--- a/pypsa/io.py
+++ b/pypsa/io.py
@@ -24,6 +24,7 @@
 import pandas as pd
 import validators
 import xarray as xr
+from deprecation import deprecated
 from pyproj import CRS
 
 from pypsa.descriptors import update_linkports_component_attrs
@@ -923,12 +924,12 @@ def _import_from_importer(
         if component == "Link":
             update_linkports_component_attrs(network, where=df)
 
-        import_components_from_dataframe(network, df, component)
+        _import_components_from_dataframe(network, df, component)
 
         if not skip_time:
             for attr, df in importer.get_series(list_name):
                 df.set_index(network.snapshots, inplace=True)
-                import_series_from_dataframe(network, df, component, attr)
+                _import_series_from_dataframe(network, df, component, attr)
 
         logger.debug(getattr(network, list_name))
 
@@ -940,15 +941,52 @@ def _import_from_importer(
     )
 
 
+def _sort_attrs(df: pd.DataFrame, attrs_list: list[str], axis: int) -> pd.DataFrame:
+    """
+    Sort axis of DataFrame according to the order of attrs_list.
+
+    Attributes not in attrs_list are appended at the end. Attributes in the list but
+    not in the DataFrame are ignored.
+
+    Parameters
+    ----------
+    df : pandas.DataFrame
+        DataFrame to sort
+    attrs_list : list
+        List of attributes to sort by
+    axis : int
+        Axis to sort (0 for index, 1 for columns)
+
+    Returns
+    -------
+    pandas.DataFrame
+    """
+
+    df_cols_set = set(df.columns if axis == 1 else df.index)
+
+    existing_cols = [col for col in attrs_list if col in df_cols_set]
+    remaining_cols = list(df_cols_set - set(attrs_list))
+
+    return df.reindex(existing_cols + remaining_cols, axis=axis)
+
+
+@deprecated(
+    deprecated_in="0.29",
+    removed_in="1.0",
+    details="Use `n.add` instead. E.g. `n.add(class_name, df.index, **df)`.",
+)
 def import_components_from_dataframe(
     network: Network, dataframe: pd.DataFrame, cls_name: str
 ) -> None:
     """
     Import components from a pandas DataFrame.
 
-    If columns are missing then defaults are used.
+    This function is deprecated. Use :py:meth`pypsa.Network.add` instead. To get the
+    same behavior for importing components from a DataFrame, use
+    `network.add(cls_name, df.index, **df)`.
 
-    If extra columns are added, these are left in the resulting component dataframe.
+    If columns are missing then defaults are used. If extra columns are added, these
+    are left in the resulting component dataframe.
 
     Parameters
     ----------
@@ -963,30 +1001,97 @@ def import_components_from_dataframe(
     >>> import pandas as pd
     >>> buses = ['Berlin', 'Frankfurt', 'Munich', 'Hamburg']
     >>> network.import_components_from_dataframe(
-            pd.DataFrame({"v_nom" : 380, "control" : 'PV'},
-                        index=buses),
-                        "Bus")
+    ...     pd.DataFrame({"v_nom" : 380, "control" : 'PV'},
+    ...                 index=buses),
+    ...                 "Bus")
     >>> network.import_components_from_dataframe(
-            pd.DataFrame({"carrier" : "solar", "bus" : buses, "p_nom_extendable" : True},
-                        index=[b+" PV" for b in buses]),
-                        "Generator")
+    ...     pd.DataFrame({"carrier" : "solar", "bus" : buses, "p_nom_extendable" : True},
+    ...                 index=[b+" PV" for b in buses]),
+    ...                 "Generator")
 
     See Also
     --------
     pypsa.Network.madd
     """
-    attrs = network.components[cls_name]["attrs"]
+    _import_components_from_dataframe(network, dataframe, cls_name)
+
+
+@deprecated(
+    deprecated_in="0.29",
+    removed_in="1.0",
+    details="Use `n.add` instead.",
+)
+def import_series_from_dataframe(
+    network: Network, dataframe: pd.DataFrame, cls_name: str, attr: str
+) -> None:
+    """
+    Import time series from a pandas DataFrame.
+
+    This function is deprecated. Use :py:meth:`pypsa.Network.add` instead, but it will
+    not work with the same data structure. To get a similar behavior, use
+    `n.pnl(class_name)[attr] = df` but make sure that the index is aligned. Also note
+    that this is overwriting the attribute dataframe, not adding to it as before.
+    It is better to use :py:meth:`pypsa.Network.add` to import time series data.
+
+    Parameters
+    ----------
+    dataframe : pandas.DataFrame
+        A DataFrame whose index is ``network.snapshots`` and
+        whose columns are a subset of the relevant components.
+    cls_name : string
+        Name of class of component
+    attr : string
+        Name of time-varying series attribute
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> network.set_snapshots(range(10))
+    >>> network.import_series_from_dataframe(
+    ...     pd.DataFrame(np.random.rand(10, 4),
+    ...         columns=network.generators.index,
+    ...                     index=range(10)),
+    ...                 "Generator",
+    ...                 "p_max_pu")
+
+    --------
+    """
+    _import_series_from_dataframe(network, dataframe, cls_name, attr)
+
+
+def _import_components_from_dataframe(
+    n: Network, dataframe: pd.DataFrame, cls_name: str, overwrite: bool = False
+) -> None:
+    """
+    Import components from a pandas DataFrame.
+
+    If columns are missing then defaults are used.
+
+    If extra columns are added, these are left in the resulting component dataframe.
+
+    Parameters
+    ----------
+    dataframe : pandas.DataFrame
+        A DataFrame whose index is the names of the components and
+        whose columns are the non-default attributes.
+    cls_name : string
+        Name of class of component, e.g. ``"Line", "Bus", "Generator", "StorageUnit"``
+    """
+    attrs = n.components[cls_name]["attrs"]
 
     static_attrs = attrs[attrs.static].drop("name")
     non_static_attrs = attrs[~attrs.static]
 
     if cls_name == "Link":
-        update_linkports_component_attrs(network, where=dataframe)
+        update_linkports_component_attrs(n, where=dataframe)
 
     # Clean dataframe and ensure correct types
     dataframe = pd.DataFrame(dataframe)
     dataframe.index = dataframe.index.astype(str)
 
+    # Fill nan values with default values
+    dataframe = dataframe.fillna(attrs["default"].to_dict())
+
     for k in static_attrs.index:
         if k not in dataframe.columns:
             dataframe[k] = static_attrs.at[k, "default"]
@@ -998,15 +1103,21 @@ def import_components_from_dataframe(
             if dataframe[k].dtype != static_attrs.at[k, "typ"]:
                 if static_attrs.at[k, "type"] == "geometry":
                     geometry = dataframe[k].replace({"": None, np.nan: None})
-                    dataframe[k] = gpd.GeoSeries.from_wkt(geometry)
+                    from shapely.geometry.base import BaseGeometry
+
+                    if geometry.apply(lambda x: isinstance(x, BaseGeometry)).all():
+                        dataframe[k] = gpd.GeoSeries(geometry)
+                    else:
+                        dataframe[k] = gpd.GeoSeries.from_wkt(geometry)
                 else:
                     dataframe[k] = dataframe[k].astype(static_attrs.at[k, "typ"])
 
     # check all the buses are well-defined
+    # TODO use func from consistency checks
     for attr in [attr for attr in dataframe if attr.startswith("bus")]:
         # allow empty buses for multi-ports
         port = int(attr[-1]) if attr[-1].isdigit() else 0
-        mask = ~dataframe[attr].isin(network.buses.index)
+        mask = ~dataframe[attr].isin(n.buses.index)
         if port > 1:
             mask &= dataframe[attr].ne("")
         missing = dataframe.index[mask]
@@ -1018,37 +1129,60 @@ def import_components_from_dataframe(
             )
 
     non_static_attrs_in_df = non_static_attrs.index.intersection(dataframe.columns)
-    old_df = network.df(cls_name)
+    old_df = n.df(cls_name)
     new_df = dataframe.drop(non_static_attrs_in_df, axis=1)
+
+    # Handle duplicates
+    duplicated_components = old_df.index.intersection(new_df.index)
+    if len(duplicated_components) > 0:
+        if not overwrite:
+            logger.warning(
+                "The following %s are already defined and will be skipped "
+                "(use overwrite=True to overwrite): %s",
+                n.components[cls_name]["list_name"],
+                ", ".join(duplicated_components),
+            )
+            new_df = new_df.drop(duplicated_components)
+        else:
+            old_df = old_df.drop(duplicated_components)
+
+    # Concatenate to new dataframe
     if not old_df.empty:
         new_df = pd.concat((old_df, new_df), sort=False)
 
-    if not new_df.index.is_unique:
-        logger.error(f"Error, new components for {cls_name} are not unique")
-        return
-
     if cls_name == "Shape":
-        new_df = gpd.GeoDataFrame(new_df, crs=network.crs)
+        new_df = gpd.GeoDataFrame(new_df, crs=n.crs)
+
+    # Align index (component names) and columns (attributes)
+    new_df = _sort_attrs(new_df, attrs.index, axis=1)
 
     new_df.index.name = cls_name
-    setattr(network, network.components[cls_name]["list_name"], new_df)
+    setattr(n, n.components[cls_name]["list_name"], new_df)
 
-    # now deal with time-dependent properties
+    # Now deal with time-dependent properties
 
-    pnl = network.pnl(cls_name)
+    pnl = n.pnl(cls_name)
 
     for k in non_static_attrs_in_df:
         # If reading in outputs, fill the outputs
         pnl[k] = pnl[k].reindex(
             columns=new_df.index, fill_value=non_static_attrs.at[k, "default"]
         )
-        pnl[k].loc[:, dataframe.index] = dataframe.loc[:, k].values
+        if overwrite:
+            pnl[k].loc[:, dataframe.index] = dataframe.loc[:, k].values
+        else:
+            new_components = dataframe.index.difference(duplicated_components)
+            pnl[k].loc[:, new_components] = dataframe.loc[new_components, k].values
 
-    setattr(network, network.components[cls_name]["list_name"] + "_t", pnl)
+    setattr(n, n.components[cls_name]["list_name"] + "_t", pnl)
 
 
-def import_series_from_dataframe(
-    network: Network, dataframe: pd.DataFrame, cls_name: str, attr: str
+def _import_series_from_dataframe(
+    n: Network,
+    dataframe: pd.DataFrame,
+    cls_name: str,
+    attr: str,
+    overwrite: bool = False,
 ) -> None:
     """
     Import time series from a pandas DataFrame.
@@ -1056,34 +1190,29 @@ def import_series_from_dataframe(
     Parameters
     ----------
     dataframe : pandas.DataFrame
-        A DataFrame whose index is ``network.snapshots`` and
+        A DataFrame whose index is ``n.snapshots`` and
         whose columns are a subset of the relevant components.
     cls_name : string
         Name of class of component
     attr : string
         Name of time-varying series attribute
-
-    Examples
-    --------
-    >>> import numpy as np
-    >>> network.set_snapshots(range(10))
-    >>> network.import_series_from_dataframe(
-            pd.DataFrame(np.random.rand(10, 4),
-                columns=network.generators.index,
-                            index=range(10)),
-                        "Generator",
-                        "p_max_pu")
-
-    See Also
-    --------
-    pypsa.Network.madd()
     """
-    df = network.df(cls_name)
-    pnl = network.pnl(cls_name)
-    list_name = network.components[cls_name]["list_name"]
+    df = n.df(cls_name)
+    pnl = n.pnl(cls_name)
+    list_name = n.components[cls_name]["list_name"]
+
+    if not overwrite:
+        try:
+            dataframe = dataframe.drop(
+                dataframe.columns.intersection(pnl[attr].columns), axis=1
+            )
+        except KeyError:
+            pass  # Don't drop any columns if the data doesn't exist yet
 
     dataframe.columns.name = cls_name
     dataframe.index.name = "snapshot"
+
+    # Check if components exist in static dataframe
     diff = dataframe.columns.difference(df.index)
     if len(diff) > 0:
         logger.warning(
@@ -1091,33 +1220,37 @@ def import_series_from_dataframe(
             f"are not in main components dataframe {list_name}"
         )
 
-    attrs = network.components[cls_name]["attrs"]
+    # Get all attributes for the component
+    attrs = n.components[cls_name]["attrs"]
+
+    # Add all unknown attributes to the dataframe without any checks
     expected_attrs = attrs[lambda ds: ds.type.str.contains("series")].index
     if attr not in expected_attrs:
-        pnl[attr] = dataframe
+        if overwrite or attr not in pnl:
+            pnl[attr] = dataframe
         return
 
-    attr_series = attrs.loc[attr]
-    default = attr_series.default
-    columns = dataframe.columns
-
-    diff = network.snapshots.difference(dataframe.index)
+    # Check if any snapshots are missing
+    diff = n.snapshots.difference(dataframe.index)
     if len(diff):
         logger.warning(
             f"Snapshots {diff} are missing from {attr} of {cls_name}."
-            f" Filling with default value '{default}'"
+            f" Filling with default value '{attrs.loc[attr].default}'"
         )
-        dataframe = dataframe.reindex(network.snapshots, fill_value=default)
+        dataframe = dataframe.reindex(n.snapshots, fill_value=attrs.loc[attr].default)
 
-    if not attr_series.static:
+    if not attrs.loc[attr].static:
         pnl[attr] = pnl[attr].reindex(
-            columns=df.index.union(columns), fill_value=default
+            columns=dataframe.columns.union(df.index),
+            fill_value=attrs.loc[attr].default,
         )
     else:
-        pnl[attr] = pnl[attr].reindex(columns=(pnl[attr].columns.union(columns)))
+        pnl[attr] = pnl[attr].reindex(
+            columns=(dataframe.columns.union(pnl[attr].columns))
+        )
 
-    pnl[attr].loc[network.snapshots, columns] = dataframe.loc[
-        network.snapshots, columns
+    pnl[attr].loc[n.snapshots, dataframe.columns] = dataframe.loc[
+        n.snapshots, dataframe.columns
     ]
 
 
@@ -1185,10 +1318,10 @@ def merge(
             f"{new.srid}, {other.srid}. Assuming {new.srid}."
         )
     for c in other.iterate_components(to_iterate_list):
-        new.import_components_from_dataframe(c.df, c.name)
+        new._import_components_from_dataframe(c.df, c.name)
         if with_time:
             for k, v in c.pnl.items():
-                new.import_series_from_dataframe(v, c.name, k)
+                new._import_series_from_dataframe(v, c.name, k)
 
     return None if inplace else new
 
@@ -1392,7 +1525,7 @@ def import_from_pypower_ppc(
         "Transformer",
         "ShuntImpedance",
     ]:
-        import_components_from_dataframe(
+        _import_components_from_dataframe(
             network, pdf[network.components[component]["list_name"]], component
         )
 
@@ -1609,7 +1742,7 @@ def import_from_pandapower_net(
         "Transformer",
         "ShuntImpedance",
     ]:
-        network.import_components_from_dataframe(d[component_name], component_name)
+        network._import_components_from_dataframe(d[component_name], component_name)
 
     # amalgamate buses connected by closed switches
 
diff --git a/pypsa/optimization/optimize.py b/pypsa/optimization/optimize.py
index d0c2baa14..7b4388ac8 100644
--- a/pypsa/optimization/optimize.py
+++ b/pypsa/optimization/optimize.py
@@ -770,7 +770,7 @@ def add_load_shedding(
         if buses is None:
             buses = n.buses.index
 
-        return n.madd(
+        return n.add(
             "Generator",
             buses,
             suffix,
diff --git a/pypsa/types.py b/pypsa/types.py
new file mode 100644
index 000000000..07da23f31
--- /dev/null
+++ b/pypsa/types.py
@@ -0,0 +1,30 @@
+from typing import Any
+
+import numpy as np
+import pandas as pd
+from pandas.api.types import is_list_like
+
+
+def is_1d_list_like(x: Any) -> bool:
+    """
+    Check if x is a 1D list-like object.
+
+    Parameters
+    ----------
+    x : Any
+        Object to check.
+
+    Returns
+    -------
+    bool
+        True if x is a 1D list-like object.
+    """
+
+    if isinstance(x, np.ndarray):
+        return x.ndim == 1
+
+    if isinstance(x, pd.DataFrame):
+        return False  # DataFrame has always 2 dimensions
+
+    else:
+        return is_list_like(x)


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index 395c60180..35bd0ea3d 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -107,9 +107,9 @@ def create_bbox(x, y, delta=0.1):
     # Convert to GeoSeries
     geo_series = gpd.GeoSeries(bboxes, crs="epsg:4326")
 
-    n.madd(
+    n.add(
         "Shape",
-        names=geo_series.index,
+        name=geo_series.index,
         geometry=geo_series,
         idx=geo_series.index,
         component="Bus",
diff --git a/test/test_bugs.py b/test/test_bugs.py
index 7e3d3dcea..5aaf508a1 100644
--- a/test/test_bugs.py
+++ b/test/test_bugs.py
@@ -6,6 +6,9 @@
 
 
 def test_890():
+    """
+    See https://github.com/PyPSA/PyPSA/issues/890.
+    """
     n = pypsa.examples.scigrid_de()
     n.calculate_dependent_values()
 
@@ -28,6 +31,9 @@ def test_890():
 
 
 def test_331():
+    """
+    See https://github.com/PyPSA/PyPSA/issues/331.
+    """
     n = pypsa.Network()
     n.add("Bus", "bus")
     n.add("Load", "load", bus="bus", p_set=10)
@@ -63,6 +69,7 @@ def test_nomansland_bus(caplog):
 def test_515():
     """
     Time-varying marginal costs removed.
+    See https://github.com/PyPSA/PyPSA/issues/515.
     """
     marginal_costs = [0, 10]
 
@@ -81,6 +88,7 @@ def test_515():
 def test_779():
     """
     Importing from xarray dataset.
+    See https://github.com/PyPSA/PyPSA/issues/779.
     """
     n1 = pypsa.Network()
     n1.add("Bus", "bus")
@@ -89,7 +97,7 @@ def test_779():
     n2.import_from_netcdf(xarr)
 
 
-def test_multiport_assingment_defaults_add():
+def test_multiport_assignment_defaults_single_add():
     """
     Add a single link to a network, then add a second link with additional
     ports.
@@ -104,16 +112,16 @@ def test_multiport_assingment_defaults_add():
     assert n.links.loc["link", "bus2"] == ""
 
 
-def test_multiport_assingment_defaults_madd():
+def test_multiport_assignment_defaults_multiple_add():
     """
     Add a single link to a network, then add a second link with additional
-    ports using madd.
+    ports.
 
     Check that the default values are assigned to the first link.
     """
     n = pypsa.Network()
     n.add("Bus", "bus")
     n.add("Bus", "bus2")
-    n.madd("Link", ["link"], bus0="bus", bus1="bus2")
-    n.madd("Link", ["link2"], bus0="bus", bus1="bus2", bus2="bus")
+    n.add("Link", ["link"], bus0="bus", bus1="bus2")
+    n.add("Link", ["link2"], bus0="bus", bus1="bus2", bus2="bus")
     assert n.links.loc["link", "bus2"] == ""
diff --git a/test/test_components.py b/test/test_components.py
index 373a6505b..f8ed4dcd1 100644
--- a/test/test_components.py
+++ b/test/test_components.py
@@ -2,26 +2,55 @@
 import sys
 
 import numpy as np
+import pandas as pd
 import pytest
 
 import pypsa
 
+rng = np.random.default_rng()
+
+
+def swap_df_index(df, axis=0):
+    df_swapped = df.copy()
+
+    if axis == 0:
+        # Swap rows: swap row 0 with row 1
+        df_swapped.iloc[[0, 1]] = df_swapped.iloc[[1, 0]].values
+    elif axis == 1:
+        # Swap columns: swap column 0 with column 1
+        df_swapped.iloc[:, [0, 1]] = df_swapped.iloc[:, [1, 0]].values
+    else:
+        raise ValueError(
+            f"Invalid axis {axis}. Allowed values are 0 (rows) and 1 (columns)."
+        )
+
+    return df_swapped
+
 
 @pytest.fixture
-def empty_network_5_buses():
+def n_5bus():
     # Set up empty network with 5 buses.
     network = pypsa.Network()
+    network.add("Bus", [f"bus_{i} " for i in range(5)])
+    return network
+
+
+@pytest.fixture
+def n_5bus_7sn():
+    # Set up empty network with 5 buses and 7 snapshots.
+    network = pypsa.Network()
     n_buses = 5
-    for i in range(n_buses):
-        network.add("Bus", f"bus_{i}")
+    n_snapshots = 7
+    network.add("Bus", [f"bus_{i} " for i in range(n_buses)])
+    network.set_snapshots(range(n_snapshots))
     return network
 
 
-def test_mremove(ac_dc_network):
+def test_remove(ac_dc_network):
     """
     GIVEN   the AC DC exemplary pypsa network.
 
-    WHEN    two components of Generator are removed with mremove
+    WHEN    two components of Generator are removed with remove
 
     THEN    the generator dataframe and the time-dependent generator
     dataframe should not contain the removed elements.
@@ -30,161 +59,272 @@ def test_mremove(ac_dc_network):
 
     generators = {"Manchester Wind", "Frankfurt Wind"}
 
-    network.mremove("Generator", generators)
+    network.remove("Generator", generators)
 
     assert not generators.issubset(network.generators.index)
     assert not generators.issubset(network.generators_t.p_max_pu.columns)
 
 
-def test_mremove_misspelled_component(ac_dc_network, caplog):
+def test_remove_misspelled_component(ac_dc_network):
     """
     GIVEN   the AC DC exemplary pypsa network.
 
-    WHEN    a misspelled component is removed with mremove
+    WHEN    a misspelled component is removed with remove
 
     THEN    the function should not change anything in the Line
     component dataframe and an error should be logged.
     """
     network = ac_dc_network
 
-    len_lines = len(network.lines.index)
+    with pytest.raises(ValueError):
+        network.remove("Liness", ["0", "1"])
+
 
-    network.mremove("Liness", ["0", "1"])
+def test_add_misspelled_component(n_5bus):
+    """
+    GIVEN   an empty PyPSA network with 5 buses.
 
-    assert len_lines == len(network.lines.index)
-    assert caplog.records[-1].levelname == "ERROR"
+    WHEN    multiple components of a misspelled component are added
+
+    THEN    the function should not change anything and an error should
+    be logged.
+    """
+    misspelled_component = "Generatro"
+    with pytest.raises(ValueError, match="not found"):
+        n_5bus.add(
+            misspelled_component,
+            ["g_1", "g_2"],
+            bus=["bus_1", "bus_2"],
+        )
 
 
-def test_madd_static(empty_network_5_buses):
+def test_add_duplicated_names(n_5bus):
     """
     GIVEN   an empty PyPSA network with 5 buses.
 
-    WHEN    multiple components of Load are added to the network with
-    madd and attribute p_set
+    WHEN    adding generators with the same name
 
-    THEN    the corresponding load components should be in the index of
-    the static load dataframe. Also the column p_set should contain any
-    value greater than 0.
+    THEN    the function should fail and an error should be logged.
     """
-    buses = empty_network_5_buses.buses.index
+    with pytest.raises(ValueError, match="must be unique"):
+        n_5bus.add(
+            "Generator",
+            ["g_1", "g_1"],
+            bus=["bus_1", "bus_2"],
+        )
+
+
+@pytest.mark.parametrize("slicer", [0, slice(0, 1), slice(None, None)])
+def test_add_static(n_5bus, slicer):
+    buses = n_5bus.buses.index[slicer]
 
-    # Add load components at every bus with attribute p_set.
     load_names = "load_" + buses
-    empty_network_5_buses.madd(
+
+    n_5bus.add("Load", load_names, bus=buses, p_set=3)
+
+    if slicer == 0:
+        load_names = pd.Index([load_names])
+
+    assert len(n_5bus.loads) == len(load_names)
+    assert n_5bus.loads.index.name == "Load"
+    assert n_5bus.loads.index.equals(load_names)
+    assert (n_5bus.loads.bus == buses).all()
+    assert (n_5bus.loads.p_set == 3).all()
+
+    if slicer == slice(None, None):
+        # Test different names shape
+        with pytest.raises(ValueError, match="for each component name."):
+            n_5bus.add("Load", load_names[1:] + "_a", bus=buses)
+
+
+@pytest.mark.parametrize("slicer", [slice(0, 1), slice(None, None)])
+def test_add_static_with_index(n_5bus, slicer):
+    buses = n_5bus.buses.index[slicer]
+
+    load_names = "load_" + buses
+    buses = pd.Series(buses, index=load_names)
+
+    n_5bus.add(
         "Load",
         load_names,
         bus=buses,
         p_set=3,
     )
 
-    assert load_names.equals(empty_network_5_buses.loads.index)
-    assert (empty_network_5_buses.loads.p_set == 3).all()
+    assert len(n_5bus.loads) == len(load_names)
+    assert n_5bus.loads.index.name == "Load"
+    assert n_5bus.loads.index.equals(load_names)
+    assert (n_5bus.loads.bus == buses).all()
+    assert (n_5bus.loads.p_set == 3).all()
 
+    if len(buses) > 1:
+        # Test unaligned names index
+        with pytest.raises(ValueError, match="index which does not align"):
+            n_5bus.add("Load", load_names + "_a", bus=swap_df_index(buses))
 
-def test_madd_t(empty_network_5_buses):
-    """
-    GIVEN   an empty PyPSA network with 5 buses and 7 snapshots.
 
-    WHEN    multiple components of Load are added to the network with
-    madd and attribute p_set
+def test_add_varying_single(n_5bus_7sn):
+    buses = n_5bus_7sn.buses.index
+
+    # Add load component at every bus with time-dependent attribute p_set.
+    p_set = rng.random(size=(len(n_5bus_7sn.snapshots)))
+    n_5bus_7sn.add(
+        "Load",
+        "load_1",
+        bus=buses[0],
+        p_set=p_set,
+    )
+
+    assert len(n_5bus_7sn.loads) == 1
+    assert n_5bus_7sn.loads.index.name == "Load"
+    assert (n_5bus_7sn.loads.index == "load_1").all()
+    assert (n_5bus_7sn.loads.bus == buses[0]).all()
+    assert (n_5bus_7sn.loads_t.p_set.T == p_set).all().all()
+    assert (n_5bus_7sn.loads.p_set == 0).all()  # Assert that default value is set
+
+    # Test different snapshots shape
+    with pytest.raises(ValueError, match="for each snapshot"):
+        n_5bus_7sn.add(
+            "Load",
+            "load_1_a",
+            p_set=p_set[1:],
+        )
 
-    THEN    the corresponding load components should be in the columns
-    of the time-dependent load_t dataframe. Also, the shape of the
-    dataframe should resemble 7 snapshots x 5 buses.
-    """
-    # Set up empty network with 5 buses and 7 snapshots.
-    snapshots = range(7)
-    empty_network_5_buses.set_snapshots(snapshots)
-    buses = empty_network_5_buses.buses.index
+
+@pytest.mark.parametrize("slicer", [slice(0, 1), slice(None, None)])
+def test_add_varying_multiple(n_5bus_7sn, slicer):
+    buses = n_5bus_7sn.buses.index[slicer]
 
     # Add load component at every bus with time-dependent attribute p_set.
     load_names = "load_" + buses
-    rng = np.random.default_rng()  # Create a random number generator
-    empty_network_5_buses.madd(
+    p_set = rng.random(size=(len(n_5bus_7sn.snapshots), len(buses)))
+    n_5bus_7sn.add(
         "Load",
         load_names,
         bus=buses,
-        p_set=rng.random(size=(len(snapshots), len(buses))),
+        p_set=p_set,
     )
 
-    assert load_names.equals(empty_network_5_buses.loads_t.p_set.columns)
-    assert empty_network_5_buses.loads_t.p_set.shape == (len(snapshots), len(buses))
+    assert len(n_5bus_7sn.loads) == len(load_names)
+    assert n_5bus_7sn.loads.index.name == "Load"
+    assert n_5bus_7sn.loads.index.equals(load_names)
+    assert (n_5bus_7sn.loads.bus == buses).all()
+    assert (n_5bus_7sn.loads_t.p_set == p_set).all().all()
+    assert (n_5bus_7sn.loads.p_set == 0).all()  # Assert that default value is set
 
+    if len(buses) > 1:
+        # Test different names shape
+        with pytest.raises(ValueError, match="but expected"):
+            n_5bus_7sn.add("Load", load_names[1:] + "_a", p_set=p_set)
 
-def test_madd_misspelled_component(empty_network_5_buses, caplog):
-    """
-    GIVEN   an empty PyPSA network with 5 buses.
+    # Test different snapshots shape
+    with pytest.raises(ValueError, match="but expected"):
+        n_5bus_7sn.add("Load", load_names + "_c", p_set=p_set[1:])
 
-    WHEN    multiple components of a misspelled component are added
 
-    THEN    the function should not change anything and an error should
-    be logged.
-    """
-    misspelled_component = "Generatro"
-    empty_network_5_buses.madd(
-        misspelled_component,
-        ["g_1", "g_2"],
-        bus=["bus_1", "bus_2"],
+def test_add_varying_multiple_with_index(n_5bus_7sn):
+    buses = n_5bus_7sn.buses.index
+
+    # Add load component at every bus with time-dependent attribute p_set.
+    load_names = "load_" + buses
+    p_set = pd.DataFrame(
+        rng.random(size=(len(n_5bus_7sn.snapshots), len(buses))),
+        index=n_5bus_7sn.snapshots,
+        columns=load_names,
     )
 
-    assert empty_network_5_buses.generators.empty
-    assert caplog.records[-1].levelname == "ERROR"
-    assert caplog.records[-1].message == (
-        f"Component class {misspelled_component} not found"
+    n_5bus_7sn.add(
+        "Load",
+        load_names,
+        bus=buses,
+        p_set=p_set,
     )
 
+    assert len(n_5bus_7sn.loads) == len(load_names)
+    assert n_5bus_7sn.loads.index.name == "Load"
+    assert n_5bus_7sn.loads.index.equals(load_names)
+    assert (n_5bus_7sn.loads.bus == buses).all()
+    assert (n_5bus_7sn.loads_t.p_set == p_set).all().all()
+    assert (n_5bus_7sn.loads.p_set == 0).all()  # Assert that default value is set
 
-def test_madd_duplicated_index(empty_network_5_buses, caplog):
-    """
-    GIVEN   an empty PyPSA network with 5 buses.
+    # Test different names shape
+    with pytest.raises(ValueError, match="index which does not align"):
+        n_5bus_7sn.add("Load", load_names[1:] + "_a", p_set=p_set)
 
-    WHEN    adding generators with the same name
+    # Test unaligned names index
+    with pytest.raises(ValueError, match="index which does not align"):
+        n_5bus_7sn.add("Load", load_names + "_b", p_set=swap_df_index(p_set))
 
-    THEN    the function should fail and an error should be logged.
-    """
-    empty_network_5_buses.madd(
-        "Generator",
-        ["g_1", "g_1"],
-        bus=["bus_1", "bus_2"],
-    )
+    # Test different snapshots shape
+    with pytest.raises(ValueError, match="index which does not align"):
+        n_5bus_7sn.add("Load", load_names + "_c", p_set=p_set[1:])
 
-    assert caplog.records[-1].levelname == "ERROR"
-    assert caplog.records[-1].message == (
-        "Error, new components for Generator are not unique"
-    )
+    # Test different snapshots index
+    with pytest.raises(ValueError, match="index which does not align"):
+        n_5bus_7sn.add("Load", load_names + "_d", p_set=swap_df_index(p_set, axis=1))
+
+
+def test_add_overwrite_static(n_5bus, caplog):
+    n_5bus.add("Bus", [f"bus_{i} " for i in range(6)], x=1)
+
+    assert (n_5bus.buses.iloc[:5].x == 0).all()
+    assert (n_5bus.buses.iloc[5].x == 1).all()
+    assert caplog.records[-1].levelname == "WARNING"
+
+    n_5bus.add("Bus", [f"bus_{i} " for i in range(5)], x=1, overwrite=True)
+    assert (n_5bus.buses.x == 1).all()
+
+
+def test_add_overwrite_varying(n_5bus_7sn, caplog):
+    bus_names = [f"bus_{i} " for i in range(6)]
+
+    n_5bus_7sn.add("Bus", bus_names, p=[1] * 6)
+    assert (n_5bus_7sn.buses_t.p.iloc[:, :5] == 0).all().all()
+    assert (n_5bus_7sn.buses_t.p.iloc[:, 5] == 1).all().all()
+    assert caplog.records[-1].levelname == "WARNING"
+
+    n_5bus_7sn.add("Bus", bus_names[:5], p=[2] * 5, overwrite=True)
+    assert (n_5bus_7sn.buses_t.p.loc[:, bus_names[:5]] == 2).all().all()
+    assert (n_5bus_7sn.buses_t.p.loc[:, bus_names[5]] == 1).all().all()
+
+    p = rng.random(size=(7, 5))
+    n_5bus_7sn.add("Bus", bus_names[:5], p=p, overwrite=False)
+    assert (n_5bus_7sn.buses_t.p.loc[:, bus_names[:5]] == 2).all().all()
+    n_5bus_7sn.add("Bus", bus_names[:5], p=p, overwrite=True)
+    assert (n_5bus_7sn.buses_t.p.loc[:, bus_names[:5]] == p).all().all()
 
 
-def test_madd_defaults(empty_network_5_buses):
+def test_multiple_add_defaults(n_5bus):
     """
     GIVEN   an empty PyPSA network with 5 buses.
 
-    WHEN    adding multiple components of Generator and Load with madd
+    WHEN    adding multiple components of Generator and Load with add
 
     THEN    the defaults should be set correctly according to
     n.component_attrs.
     """
     gen_names = ["g_1", "g_2"]
-    empty_network_5_buses.madd(
+    n_5bus.add(
         "Generator",
         gen_names,
         bus=["bus_1", "bus_2"],
     )
 
     line_names = ["l_1", "l_2"]
-    empty_network_5_buses.madd(
+    n_5bus.add(
         "Load",
         line_names,
         bus=["bus_1", "bus_2"],
     )
 
     assert (
-        empty_network_5_buses.generators.loc[gen_names[0], "control"]
-        == empty_network_5_buses.component_attrs.Generator.loc["control", "default"]
+        n_5bus.generators.loc[gen_names[0], "control"]
+        == n_5bus.component_attrs.Generator.loc["control", "default"]
     )
 
     assert (
-        empty_network_5_buses.loads.loc[line_names[0], "p_set"]
-        == empty_network_5_buses.component_attrs.Load.loc["p_set", "default"]
+        n_5bus.loads.loc[line_names[0], "p_set"]
+        == n_5bus.component_attrs.Load.loc["p_set", "default"]
     )
 
 
@@ -250,7 +390,7 @@ def test_copy_snapshots(all_networks):
         assert copied_network == network
 
 
-def test_add_network_static(ac_dc_network, empty_network_5_buses):
+def test_single_add_network_static(ac_dc_network, n_5bus):
     """
     GIVEN   the AC DC exemplary pypsa network and an empty PyPSA network with 5
     buses.
@@ -261,12 +401,12 @@ def test_add_network_static(ac_dc_network, empty_network_5_buses):
     also the buses in the second network
     """
 
-    n = ac_dc_network.merge(empty_network_5_buses, with_time=False)
+    n = ac_dc_network.merge(n_5bus, with_time=False)
     new_buses = set(n.buses.index)
-    assert new_buses.issuperset(empty_network_5_buses.buses.index)
+    assert new_buses.issuperset(n_5bus.buses.index)
 
 
-def test_add_network_with_time(ac_dc_network, empty_network_5_buses):
+def test_single_add_network_with_time(ac_dc_network, n_5bus):
     """
     GIVEN   the AC DC exemplary pypsa network and an empty PyPSA network with 5
     buses and the same snapshots.
@@ -277,12 +417,12 @@ def test_add_network_with_time(ac_dc_network, empty_network_5_buses):
     also the buses in the second network
     """
     with pytest.raises(ValueError):
-        ac_dc_network.merge(empty_network_5_buses, with_time=True)
+        ac_dc_network.merge(n_5bus, with_time=True)
 
-    empty_network_5_buses.set_snapshots(ac_dc_network.snapshots)
-    n = ac_dc_network.merge(empty_network_5_buses, with_time=True)
+    n_5bus.set_snapshots(ac_dc_network.snapshots)
+    n = ac_dc_network.merge(n_5bus, with_time=True)
     new_buses = set(n.buses.index)
-    assert new_buses.issuperset(empty_network_5_buses.buses.index)
+    assert new_buses.issuperset(n_5bus.buses.index)
 
 
 def test_shape_reprojection(ac_dc_network_shapes):
diff --git a/test/test_lopf_basic_constraints.py b/test/test_lopf_basic_constraints.py
index e5ef270c1..b100e12d9 100644
--- a/test/test_lopf_basic_constraints.py
+++ b/test/test_lopf_basic_constraints.py
@@ -233,7 +233,7 @@ def test_optimization_with_strongly_meshed_bus():
     n.add("Load", "load", bus="bus", p_set=1)
 
     n.add("Bus", "bus2")
-    n.madd("Generator", pd.RangeIndex(50), bus="bus2", p_nom=1, marginal_cost=10)
+    n.add("Generator", pd.RangeIndex(50), bus="bus2", p_nom=1, marginal_cost=10)
     n.add("Load", "load2", bus="bus2", p_set=1)
 
     n.add("Line", "line", bus0="bus", bus1="bus2", s_nom=1)
diff --git a/test/test_lopf_iteratively.py b/test/test_lopf_iteratively.py
index df1365ea9..81c0910d7 100644
--- a/test/test_lopf_iteratively.py
+++ b/test/test_lopf_iteratively.py
@@ -4,7 +4,7 @@
 def test_optimize_post_discretization():
     n = pypsa.Network()
 
-    n.madd("Bus", ["a", "b", "c"], v_nom=380.0)
+    n.add("Bus", ["a", "b", "c"], v_nom=380.0)
     n.add("Generator", "generator", bus="a", p_nom=900.0, marginal_cost=10.0)
     n.add("Load", "load", bus="c", p_set=900.0)
     n.add(
diff --git a/test/test_lopf_multiinvest.py b/test/test_lopf_multiinvest.py
index cbf67a5bc..f3d2890fb 100644
--- a/test/test_lopf_multiinvest.py
+++ b/test/test_lopf_multiinvest.py
@@ -21,11 +21,11 @@ def n():
     n = pypsa.Network(snapshots=range(10))
     n.investment_periods = [2020, 2030, 2040, 2050]
     n.add("Carrier", "gencarrier")
-    n.madd("Bus", [1, 2])
+    n.add("Bus", [1, 2])
 
     for i, period in enumerate(n.investment_periods):
         factor = (10 + i) / 10
-        n.madd(
+        n.add(
             "Generator",
             [f"gen1-{period}", f"gen2-{period}"],
             bus=[1, 2],
@@ -53,7 +53,7 @@ def n():
 
     load = range(100, 100 + len(n.snapshots))
     load = pd.DataFrame({"load1": load, "load2": load}, index=n.snapshots)
-    n.madd(
+    n.add(
         "Load",
         ["load1", "load2"],
         bus=[1, 2],
@@ -67,7 +67,7 @@ def n():
 def n_sus(n):
     # only keep generators which are getting more expensiv and push generator
     # capital cost, so that sus are activated
-    n.mremove("Generator", n.generators.query('bus == "1"').index)
+    n.remove("Generator", n.generators.query('bus == "1"').index)
     n.generators.capital_cost *= 5
 
     for i, period in enumerate(n.investment_periods):
@@ -89,7 +89,7 @@ def n_sus(n):
 def n_sts(n):
     # only keep generators which are getting more expensiv and push generator
     # capital cost, so that sus are activated
-    n.mremove("Generator", n.generators.query('bus == "1"').index)
+    n.remove("Generator", n.generators.query('bus == "1"').index)
     n.generators.capital_cost *= 5
 
     n.add("Bus", "1 battery")
diff --git a/test/test_lopf_unit_commitment.py b/test/test_lopf_unit_commitment.py
index dd8d61119..4dc19effb 100644
--- a/test/test_lopf_unit_commitment.py
+++ b/test/test_lopf_unit_commitment.py
@@ -421,7 +421,7 @@ def test_link_unit_commitment():
 
     n.set_snapshots(snapshots)
 
-    n.madd("Bus", ["gas", "electricity"])
+    n.add("Bus", ["gas", "electricity"])
 
     n.add("Generator", "gas", bus="gas", marginal_cost=10, p_nom=20000)