Repository: geopandas/geopandas

Issue:
API: default behaviour of alignment in binary ops
#1668 introduces `align` keyword to control whether two GeoSeries gets aligned in binary, row by row operations or not. However, the default behaviour may require a bit more discussion.

Moving from the original PR here (from https://github.com/geopandas/geopandas/pull/1668#discussion_r578692627).

> @jorisvandenbossche:  Idea: make the default None, which is treated as True but gives a warning when alignment is actually done. This gives a way for the user to indicate they are OK with alignment without getting warnings about it.
(small disadvantage is that the signature in the function is less explicit than with align=True)

> @martinfleis: I do understand your point, but considering how troublesome this behaviour is (I responded to countless questions on this), I would prefer having an explicit align=True in the signature. Having None/True/False is a nice trick for advanced users to silence warnings, but in this specific case, I would prefer understanding for basic users (I really don't want to answer the same question again :D).

> @jorisvandenbossche: Then we should maybe rather make it an error by default? To make it really clear for users they are doing something wrong (with a good explanation then)

> @martinfleis: That'd be a breaking change.




Discussion:
Looking at the original issue again, I think this is actually covered by that: https://github.com/geopandas/geopandas/issues/750 (but fine to close that one as well, doesn't matter ;))

That would indeed be a breaking change, but if we want to do this, we can first raise a more elaborate FutureWarning (compared to the UserWarning we have now)
We should make the warning more explicit and explain what happens apart from just saying that the indices are different.
First attempt:

> The index of the left and right GeoSeries in the 'left.contains(right)' operation is not equal, and therefore they will be aligned (reordering and/or introducing missing values) before executing the operation. 
If this alignment is the expected (or desired?) behaviour, you can silence this warning by passing 'align=True'. If you don't want alignment and protect yourself of accidentally aligning, you can pass 'align=False'.


> First attempt:
> 
> > The index of the left and right GeoSeries in the 'left.contains(right)' operation is not equal, and therefore they will be aligned (reordering and/or introducing missing values) before executing the operation.
> > If this alignment is the expected (or desired?) behaviour, you can silence this warning by passing 'align=True'. If you don't want alignment and protect yourself of accidentally aligning, you can pass 'align=False'.

I was going to look at this again as it seems nice to try and get an improved error message in - but the suggested message doesn't correspond to the behaviour in the code. Currently there's no way to silence the warning, it was chosen not to implement that as an option. The suggestion in #2582 the same as originally suggested https://github.com/geopandas/geopandas/pull/1668#discussion_r578692627, so that's already been considered. 

A less elegant version of this with True as an explicit default would be to add `"align_no_warn"` as a value for align.

Otherwise, we could just include the updated warning with a snippet for silencing the warning using warnings.filterwarnings?



The agreement during the last dev meeting was to use `None` as a defaulting value defaulting to True. Warning is shown when `align=None` and can be silenced by explicitly stating `align=True`. I will make a PR for that.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab8cbb04eb..d414e45c55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@ Notes on dependencies:
 API changes:
 
 - `unary_union` is now deprecated and replaced by the `union_all` method (#3007).
+- `align` keyword in binary methods now defaults to `None`, treated as True. Explicit True
+  will silence the warning about mismachted indices. (#3212)
 - The `sjoin` method will now preserve the name of the index of the right
   GeoDataFrame, if it has one, instead of always using `"index_right"` as the
   name for the resulting column in the return value (#846, #2144).
@@ -41,7 +43,7 @@ New features and improvements:
   `buffer(0)` (#3113).
 - Passing `"geometry"` as `dtype` to `pd.read_csv` will now return a GeoSeries for
   the specified columns (#3101).
-- 
+-
 API changes:
 - Added support for ``mask`` keyword for pyogrio engine for pyogrio >= 0.7.0 (#3062).
 
@@ -67,16 +69,16 @@ Deprecations and compatibility notes:
   removed. New sample datasets are now available in the
   [geodatasets](https://geodatasets.readthedocs.io/en/latest/) package (#3084).
 - Many longstanding deprecated functions, methods and properties have been removed (#3174)
-  - Removed deprecated functions  
-    `geopandas.io.read_file`, `geopandas.io.to_file` and `geopandas.io.sql.read_postgis`. 
-    `geopandas.read_file`, `geopandas.read_postgis` and the GeoDataFrame/GeoSeries `to_file(..)` 
+  - Removed deprecated functions
+    `geopandas.io.read_file`, `geopandas.io.to_file` and `geopandas.io.sql.read_postgis`.
+    `geopandas.read_file`, `geopandas.read_postgis` and the GeoDataFrame/GeoSeries `to_file(..)`
     method should be used instead.
   - Removed deprecated `GeometryArray.data` property, `np.asarray(..)` or the `to_numpy()`
     method should be used instead.
   - Removed deprecated `sindex.query_bulk` method, using `sindex.query` instead.
   - Removed deprecated `sjoin` parameter `op`, `predicate` should be supplied instead.
-  - Removed deprecated GeoSeries/ GeoDataFrame methods `__xor__`, `__or__`, `__and__` and 
-    `__sub__`. Instead use methods `symmetric_difference`, `union`, `intersection` and 
+  - Removed deprecated GeoSeries/ GeoDataFrame methods `__xor__`, `__or__`, `__and__` and
+    `__sub__`. Instead use methods `symmetric_difference`, `union`, `intersection` and
     `difference` respectively.
 - Fixes for compatibility with psycopg (#3167).
 
diff --git a/geopandas/base.py b/geopandas/base.py
index dfa59bc477..c0473f083b 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -27,13 +27,24 @@ def is_geometry_type(data):
 
 def _delegate_binary_method(op, this, other, align, *args, **kwargs):
     # type: (str, GeoSeries, GeoSeries) -> GeoSeries/Series
+    if align is None:
+        align = True
+        maybe_warn = True
+    else:
+        maybe_warn = False
     this = this.geometry
     if isinstance(other, GeoPandasBase):
         if align and not this.index.equals(other.index):
-            warn(
-                "The indices of the two GeoSeries are different.",
-                stacklevel=4,
-            )
+            if maybe_warn:
+                warn(
+                    "The indices of the left and right GeoSeries' are not equal, and "
+                    "therefore they will be aligned (reordering and/or introducing "
+                    "missing values) before executing the operation. If this alignment "
+                    "is the desired behaviour, you can silence this warning by passing "
+                    "'align=True'. If you don't want alignment and protect yourself of "
+                    "accidentally aligning, you can pass 'align=False'.",
+                    stacklevel=4,
+                )
             this, other = this.align(other.geometry)
         else:
             other = other.geometry
@@ -1670,7 +1681,7 @@ def union_all(self):
     # Binary operations that return a pandas Series
     #
 
-    def contains(self, other, align=True):
+    def contains(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that contains `other`.
 
@@ -1693,9 +1704,9 @@ def contains(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if it
             is contained.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -1786,7 +1797,7 @@ def contains(self, other, align=True):
         """
         return _binary_op("contains", self, other, align)
 
-    def contains_properly(self, other, align=True):
+    def contains_properly(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that is completely inside ``other``, with no common
         boundary points.
@@ -1806,9 +1817,9 @@ def contains_properly(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if it
             is contained.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -1907,7 +1918,7 @@ def contains_properly(self, other, align=True):
         """
         return _binary_op("contains_properly", self, other, align)
 
-    def dwithin(self, other, distance, align=True):
+    def dwithin(self, other, distance, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that is within a set distance from ``other``.
 
@@ -1926,9 +1937,9 @@ def dwithin(self, other, distance, align=True):
             applied to all geometries. An array or Series will be applied elementwise.
             If np.array or pd.Series are used then it must have same length as the
             GeoSeries.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2018,7 +2029,7 @@ def dwithin(self, other, distance, align=True):
         """
         return _binary_op("dwithin", self, other, distance=distance, align=align)
 
-    def geom_equals(self, other, align=True):
+    def geom_equals(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry equal to `other`.
 
@@ -2036,9 +2047,9 @@ def geom_equals(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test for
             equality.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2128,7 +2139,7 @@ def geom_equals(self, other, align=True):
         """
         return _binary_op("geom_equals", self, other, align)
 
-    def geom_almost_equals(self, other, decimal=6, align=True):
+    def geom_almost_equals(self, other, decimal=6, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` if
         each aligned geometry is approximately equal to `other`.
 
@@ -2146,9 +2157,9 @@ def geom_almost_equals(self, other, decimal=6, align=True):
             The GeoSeries (elementwise) or geometric object to compare to.
         decimal : int
             Decimal place precision used when testing for approximate equality.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2205,7 +2216,7 @@ def geom_almost_equals(self, other, decimal=6, align=True):
             "geom_equals_exact", self, other, tolerance=tolerance, align=align
         )
 
-    def geom_equals_exact(self, other, tolerance, align=True):
+    def geom_equals_exact(self, other, tolerance, align=None):
         """Return True for all geometries that equal aligned *other* to a given
         tolerance, else False.
 
@@ -2220,9 +2231,9 @@ def geom_equals_exact(self, other, tolerance, align=True):
             The GeoSeries (elementwise) or geometric object to compare to.
         tolerance : float
             Decimal place precision used when testing for approximate equality.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2270,7 +2281,7 @@ def geom_equals_exact(self, other, tolerance, align=True):
             "geom_equals_exact", self, other, tolerance=tolerance, align=align
         )
 
-    def crosses(self, other, align=True):
+    def crosses(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that cross `other`.
 
@@ -2288,9 +2299,9 @@ def crosses(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if is
             crossed.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2382,7 +2393,7 @@ def crosses(self, other, align=True):
         """
         return _binary_op("crosses", self, other, align)
 
-    def disjoint(self, other, align=True):
+    def disjoint(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry disjoint to `other`.
 
@@ -2399,9 +2410,9 @@ def disjoint(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if is
             disjoint.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2483,7 +2494,7 @@ def disjoint(self, other, align=True):
         """
         return _binary_op("disjoint", self, other, align)
 
-    def intersects(self, other, align=True):
+    def intersects(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that intersects `other`.
 
@@ -2500,9 +2511,9 @@ def intersects(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if is
             intersected.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2594,7 +2605,7 @@ def intersects(self, other, align=True):
         """
         return _binary_op("intersects", self, other, align)
 
-    def overlaps(self, other, align=True):
+    def overlaps(self, other, align=None):
         """Returns True for all aligned geometries that overlap *other*, else False.
 
         Geometries overlaps if they have more than one but not all
@@ -2612,9 +2623,9 @@ def overlaps(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if
             overlaps.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2705,7 +2716,7 @@ def overlaps(self, other, align=True):
         """
         return _binary_op("overlaps", self, other, align)
 
-    def touches(self, other, align=True):
+    def touches(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that touches `other`.
 
@@ -2723,9 +2734,9 @@ def touches(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if is
             touched.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2817,7 +2828,7 @@ def touches(self, other, align=True):
         """
         return _binary_op("touches", self, other, align)
 
-    def within(self, other, align=True):
+    def within(self, other, align=None):
         """Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that is within `other`.
 
@@ -2839,9 +2850,9 @@ def within(self, other, align=True):
         other : GeoSeries or geometric object
             The GeoSeries (elementwise) or geometric object to test if each
             geometry is within.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -2931,7 +2942,7 @@ def within(self, other, align=True):
         """
         return _binary_op("within", self, other, align)
 
-    def covers(self, other, align=True):
+    def covers(self, other, align=None):
         """
         Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that is entirely covering `other`.
@@ -2953,9 +2964,9 @@ def covers(self, other, align=True):
         ----------
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to check is being covered.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -3045,7 +3056,7 @@ def covers(self, other, align=True):
         """
         return _binary_op("covers", self, other, align)
 
-    def covered_by(self, other, align=True):
+    def covered_by(self, other, align=None):
         """
         Returns a ``Series`` of ``dtype('bool')`` with value ``True`` for
         each aligned geometry that is entirely covered by `other`.
@@ -3066,9 +3077,9 @@ def covered_by(self, other, align=True):
         ----------
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to check is being covered.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -3159,7 +3170,7 @@ def covered_by(self, other, align=True):
         """
         return _binary_op("covered_by", self, other, align)
 
-    def distance(self, other, align=True):
+    def distance(self, other, align=None):
         """Returns a ``Series`` containing the distance to aligned `other`.
 
         The operation works on a 1-to-1 row-wise manner:
@@ -3172,9 +3183,9 @@ def distance(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             distance to.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
 
         Returns
@@ -3271,9 +3282,9 @@ def hausdorff_distance(self, other, align=True, densify=None):
         other : GeoSeries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             distance to.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
         densify : float (default None)
             A value between 0 and 1, that splits each subsegment of a line string
             into equal length segments, making the approximation less coarse.
@@ -3393,9 +3404,9 @@ def frechet_distance(self, other, align=True, densify=None):
         other : GeoSeries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             distance to.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
         densify : float (default None)
             A value between 0 and 1, that splits each subsegment of a line string
             into equal length segments, making the approximation less coarse.
@@ -3494,7 +3505,7 @@ def frechet_distance(self, other, align=True, densify=None):
     # Binary operations that return a GeoSeries
     #
 
-    def difference(self, other, align=True):
+    def difference(self, other, align=None):
         """Returns a ``GeoSeries`` of the points in each aligned geometry that
         are not in `other`.
 
@@ -3511,9 +3522,9 @@ def difference(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             difference to.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -3605,7 +3616,7 @@ def difference(self, other, align=True):
         """
         return _binary_geo("difference", self, other, align)
 
-    def symmetric_difference(self, other, align=True):
+    def symmetric_difference(self, other, align=None):
         """Returns a ``GeoSeries`` of the symmetric difference of points in
         each aligned geometry with `other`.
 
@@ -3626,9 +3637,9 @@ def symmetric_difference(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             symmetric difference to.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -3720,7 +3731,7 @@ def symmetric_difference(self, other, align=True):
         """
         return _binary_geo("symmetric_difference", self, other, align)
 
-    def union(self, other, align=True):
+    def union(self, other, align=None):
         """Returns a ``GeoSeries`` of the union of points in each aligned geometry with
         `other`.
 
@@ -3738,9 +3749,9 @@ def union(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the union
             with.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -3834,7 +3845,7 @@ def union(self, other, align=True):
         """
         return _binary_geo("union", self, other, align)
 
-    def intersection(self, other, align=True):
+    def intersection(self, other, align=None):
         """Returns a ``GeoSeries`` of the intersection of points in each
         aligned geometry with `other`.
 
@@ -3852,9 +3863,9 @@ def intersection(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             intersection with.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -4016,7 +4027,7 @@ def clip_by_rect(self, xmin, ymin, xmax, ymax):
         clipped_geometry = geometry_array.clip_by_rect(xmin, ymin, xmax, ymax)
         return GeoSeries(clipped_geometry, index=self.index, crs=self.crs)
 
-    def shortest_line(self, other, align=True):
+    def shortest_line(self, other, align=None):
         """
         Returns the shortest two-point line between two geometries.
 
@@ -4037,9 +4048,9 @@ def shortest_line(self, other, align=True):
         other : Geoseries or geometric object
             The Geoseries (elementwise) or geometric object to find the
             shortest line with.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -4119,7 +4130,7 @@ def shortest_line(self, other, align=True):
         """
         return _binary_geo("shortest_line", self, other, align)
 
-    def snap(self, other, tolerance, align=True):
+    def snap(self, other, tolerance, align=None):
         """Snaps an input geometry to reference geometry’s vertices.
 
         Vertices of the first geometry are snapped to vertices of the second. geometry,
@@ -4148,9 +4159,9 @@ def snap(self, other, tolerance, align=True):
             The Geoseries (elementwise) or geometric object to snap to.
         tolerance : float or array like
             Maximum distance between vertices that shall be snapped
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -4505,7 +4516,7 @@ def simplify(self, *args, **kwargs):
         """
         return _delegate_geo_method("simplify", self, *args, **kwargs)
 
-    def relate(self, other, align=True):
+    def relate(self, other, align=None):
         """
         Returns the DE-9IM intersection matrices for the geometries
 
@@ -4519,9 +4530,9 @@ def relate(self, other, align=True):
         other : BaseGeometry or GeoSeries
             The other geometry to computed
             the DE-9IM intersection matrices from.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------
@@ -4610,7 +4621,7 @@ def relate(self, other, align=True):
         """
         return _binary_op("relate", self, other, align)
 
-    def project(self, other, normalized=False, align=True):
+    def project(self, other, normalized=False, align=None):
         """
         Return the distance along each geometry nearest to *other*
 
@@ -4631,9 +4642,9 @@ def project(self, other, normalized=False, align=True):
         normalized : boolean
             If normalized is True, return the distance normalized to
             the length of the object.
-        align : bool (default True)
+        align : bool | None (default None)
             If True, automatically aligns GeoSeries based on their indices.
-            If False, the order of elements is preserved.
+            If False, the order of elements is preserved. None defaults to True.
 
         Returns
         -------


Test patch:
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index ba934c18c0..4c8d0b0ab5 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -299,15 +299,26 @@ def _test_unary(self, op, expected, a, fcmp, method=False):
     #         self._test_binary_topological('intersection', self.g3,
     #                                       self.g3, no_crs_g3)
 
+    def test_alignment_warning(self):
+        with pytest.warns(
+            UserWarning,
+            match="The indices of the left and right GeoSeries' are not equal",
+        ):
+            self.g0.intersection(self.g9, align=None)
+
+        with warnings.catch_warnings(record=True) as record:
+            self.g0.intersection(self.g9, align=True)
+            self.g0.intersection(self.g9, align=False)
+
+            assert len(record) == 0
+
     def test_intersection(self):
         self._test_binary_topological("intersection", self.t1, self.g1, self.g2)
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            self._test_binary_topological(
-                "intersection", self.all_none, self.g1, self.empty
-            )
+        self._test_binary_topological(
+            "intersection", self.all_none, self.g1, self.empty
+        )
 
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert len(self.g0.intersection(self.g9, align=True) == 8)
+        assert len(self.g0.intersection(self.g9, align=True) == 8)
         assert len(self.g0.intersection(self.g9, align=False) == 7)
 
     def test_clip_by_rect(self):
@@ -322,8 +333,7 @@ def test_clip_by_rect(self):
     def test_union_series(self):
         self._test_binary_topological("union", self.sq, self.g1, self.g2)
 
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert len(self.g0.union(self.g9, align=True) == 8)
+        assert len(self.g0.union(self.g9, align=True) == 8)
         assert len(self.g0.union(self.g9, align=False) == 7)
 
     def test_union_polygon(self):
@@ -332,8 +342,7 @@ def test_union_polygon(self):
     def test_symmetric_difference_series(self):
         self._test_binary_topological("symmetric_difference", self.sq, self.g3, self.g4)
 
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert len(self.g0.symmetric_difference(self.g9, align=True) == 8)
+        assert len(self.g0.symmetric_difference(self.g9, align=True) == 8)
         assert len(self.g0.symmetric_difference(self.g9, align=False) == 7)
 
     def test_symmetric_difference_poly(self):
@@ -346,8 +355,7 @@ def test_difference_series(self):
         expected = GeoSeries([GeometryCollection(), self.t2])
         self._test_binary_topological("difference", expected, self.g1, self.g2)
 
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert len(self.g0.difference(self.g9, align=True) == 8)
+        assert len(self.g0.difference(self.g9, align=True) == 8)
         assert len(self.g0.difference(self.g9, align=False) == 7)
 
     def test_difference_poly(self):
@@ -480,8 +488,7 @@ def test_contains(self):
         assert_array_dtype_equal(expected, self.g0.contains(self.t1))
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=True))
 
         expected = [False, False, True, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.contains(self.g9, align=False))
@@ -491,10 +498,9 @@ def test_contains_properly(self):
         assert_array_dtype_equal(expected, self.g0.contains_properly(Point(0.25, 0.25)))
 
         expected = [False, False, False, False, False, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(
-                expected, self.g0.contains_properly(self.g9, align=True)
-            )
+        assert_array_dtype_equal(
+            expected, self.g0.contains_properly(self.g9, align=True)
+        )
 
         expected = [False, False, True, False, False, False, False]
         assert_array_dtype_equal(
@@ -507,8 +513,7 @@ def test_dwithin(self):
         assert_array_dtype_equal(expected, self.g0.dwithin(self.p0, 6))
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.dwithin(self.g9, 1, align=True))
+        assert_array_dtype_equal(expected, self.g0.dwithin(self.g9, 1, align=True))
         expected = [True, True, True, True, False, False, False]
         assert_array_dtype_equal(expected, self.g0.dwithin(self.g9, 1, align=False))
 
@@ -539,8 +544,7 @@ def test_crosses(self):
         assert_array_dtype_equal(expected, self.crossed_lines.crosses(self.l3))
 
         expected = [False] * 8
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=True))
 
         expected = [False] * 7
         assert_array_dtype_equal(expected, self.g0.crosses(self.g9, align=False))
@@ -550,8 +554,7 @@ def test_disjoint(self):
         assert_array_dtype_equal(expected, self.g0.disjoint(self.t1))
 
         expected = [False] * 8
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=True))
 
         expected = [False, False, False, False, True, False, False]
         assert_array_dtype_equal(expected, self.g0.disjoint(self.g9, align=False))
@@ -588,8 +591,7 @@ def test_relate(self):
             index=range(8),
         )
 
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.relate(self.g9, align=True))
 
         expected = Series(
             [
@@ -615,8 +617,7 @@ def test_distance(self):
         assert_array_dtype_equal(expected, self.g6.distance(self.na_none))
 
         expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.distance(self.g9, align=True))
 
         val = self.g0.iloc[4].distance(self.g9.iloc[4])
         expected = Series(np.array([0, 0, 0, 0, val, np.nan, np.nan]), self.g0.index)
@@ -636,10 +637,9 @@ def test_hausdorff_distance(self):
         assert_array_dtype_equal(expected, self.na_none.hausdorff_distance(self.p0))
 
         expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(
-                expected, self.g0.hausdorff_distance(self.g9, align=True)
-            )
+        assert_array_dtype_equal(
+            expected, self.g0.hausdorff_distance(self.g9, align=True)
+        )
 
         val_1 = self.g0.iloc[0].hausdorff_distance(self.g9.iloc[0])
         val_2 = self.g0.iloc[2].hausdorff_distance(self.g9.iloc[2])
@@ -665,10 +665,9 @@ def test_frechet_distance(self):
         assert_array_dtype_equal(expected, self.na_none.frechet_distance(self.p0))
 
         expected = Series(np.array([np.nan, 0, 0, 0, 0, 0, np.nan, np.nan]), range(8))
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(
-                expected, self.g0.frechet_distance(self.g9, align=True)
-            )
+        assert_array_dtype_equal(
+            expected, self.g0.frechet_distance(self.g9, align=True)
+        )
 
         # expected returns
         val_1 = 1.0
@@ -704,8 +703,7 @@ def test_intersects(self):
         assert_array_dtype_equal(expected, self.g0.intersects(self.empty_poly))
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=True))
 
         expected = [True, True, True, True, False, False, False]
         assert_array_dtype_equal(expected, self.g0.intersects(self.g9, align=False))
@@ -718,8 +716,7 @@ def test_overlaps(self):
         assert_array_dtype_equal(expected, self.g4.overlaps(self.t1))
 
         expected = [False] * 8
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=True))
 
         expected = [False] * 7
         assert_array_dtype_equal(expected, self.g0.overlaps(self.g9, align=False))
@@ -729,8 +726,7 @@ def test_touches(self):
         assert_array_dtype_equal(expected, self.g0.touches(self.t1))
 
         expected = [False] * 8
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=True))
 
         expected = [True, False, False, True, False, False, False]
         assert_array_dtype_equal(expected, self.g0.touches(self.g9, align=False))
@@ -743,8 +739,7 @@ def test_within(self):
         assert_array_dtype_equal(expected, self.g0.within(self.sq))
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.within(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.within(self.g9, align=True))
 
         expected = [False, True, False, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.within(self.g9, align=False))
@@ -761,8 +756,7 @@ def test_covers(self):
         assert_series_equal(res, exp)
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=True))
 
         expected = [False, False, True, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.covers(self.g9, align=False))
@@ -778,8 +772,7 @@ def test_covered_by(self):
         assert_series_equal(res, exp)
 
         expected = [False, True, True, True, True, True, False, False]
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=True))
+        assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=True))
 
         expected = [False, True, False, False, False, False, False]
         assert_array_dtype_equal(expected, self.g0.covered_by(self.g9, align=False))
@@ -1092,8 +1085,7 @@ def test_project(self):
 
         s = GeoSeries([Point(2, 2), Point(0.5, 0.5)], index=[1, 2])
         expected = Series([np.nan, 2.0, np.nan])
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            assert_series_equal(self.g5.project(s), expected)
+        assert_series_equal(self.g5.project(s), expected)
 
         expected = Series([2.0, 0.5], index=self.g5.index)
         assert_series_equal(self.g5.project(s, align=False), expected)
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index b9f81338e7..99b8336bcd 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -110,11 +110,11 @@ def test_warning_if_not_aligned(self):
         # Test that warning is issued when operating on non-aligned series
 
         # _series_op
-        with pytest.warns(UserWarning, match="The indices .+ different"):
+        with pytest.warns(UserWarning, match="The indices .+ not equal"):
             self.a1.contains(self.a2)
 
         # _geo_op
-        with pytest.warns(UserWarning, match="The indices .+ different"):
+        with pytest.warns(UserWarning, match="The indices .+ not equal"):
             self.a1.union(self.a2)
 
     def test_no_warning_if_aligned(self):
@@ -136,8 +136,7 @@ def test_geom_equals(self):
         assert_array_equal(self.g1.geom_equals(self.sq), [False, True])
 
     def test_geom_equals_align(self):
-        with pytest.warns(UserWarning, match="The indices .+ different"):
-            a = self.a1.geom_equals(self.a2, align=True)
+        a = self.a1.geom_equals(self.a2, align=True)
         exp = pd.Series([False, True, False], index=["A", "B", "C"])
         assert_series_equal(a, exp)
 
@@ -153,7 +152,7 @@ def test_geom_almost_equals(self):
         with warnings.catch_warnings():
             warnings.filterwarnings(
                 "ignore",
-                "The indices of the two GeoSeries are different",
+                "The indices of the left and right GeoSeries' are not equal",
                 UserWarning,
             )
             assert_array_equal(
@@ -170,7 +169,9 @@ def test_geom_equals_exact(self):
         assert_array_equal(self.g1.geom_equals_exact(self.sq, 0.001), [False, True])
         with warnings.catch_warnings():
             warnings.filterwarnings(
-                "ignore", "The indices of the two GeoSeries are different", UserWarning
+                "ignore",
+                "The indices of the left and right GeoSeries' are not equal",
+                UserWarning,
             )
             assert_array_equal(
                 self.a1.geom_equals_exact(self.a2, 0.001, align=True),