in one line, or if the inner expression doesn't have any delimiters to
 further split on.  Otherwise, the parentheses are always added.
 
+### Call chains
+
+Some popular APIs, like ORMs, use call chaining.  This API style is known
+as a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface).
+*Black* formats those treating dots that follow a call or an indexing
+operation like a very low priority delimiter.  It's easier to show the
+behavior than to explain it.  Look at the example::
+```py3
+def example(session):
+    result = (
+        session.query(models.Customer.id)
+        .filter(
+            models.Customer.account_id == account_id,
+            models.Customer.email == email_address,
+        )
+        .order_by(models.Customer.id.asc())
+        .all()
+    )
+```
+
 ### Typing stub files
 
 PEP 484 describes the syntax for type hints in Python.  One of the
 
 ### 18.5a0 (unreleased)
 
+* call chains are now formatted according to the [fluent interfaces](https://en.wikipedia.org/wiki/Fluent_interface) style (#67)
+
 * slices are now formatted according to PEP 8 (#178)
 
 * parentheses are now also managed automatically on the right-hand side
 
 STRING_PRIORITY = 12
 COMPARATOR_PRIORITY = 10
 MATH_PRIORITIES = {
-    token.VBAR: 8,
-    token.CIRCUMFLEX: 7,
-    token.AMPER: 6,
-    token.LEFTSHIFT: 5,
-    token.RIGHTSHIFT: 5,
-    token.PLUS: 4,
-    token.MINUS: 4,
-    token.STAR: 3,
-    token.SLASH: 3,
-    token.DOUBLESLASH: 3,
-    token.PERCENT: 3,
-    token.AT: 3,
-    token.TILDE: 2,
-    token.DOUBLESTAR: 1,
+    token.VBAR: 9,
+    token.CIRCUMFLEX: 8,
+    token.AMPER: 7,
+    token.LEFTSHIFT: 6,
+    token.RIGHTSHIFT: 6,
+    token.PLUS: 5,
+    token.MINUS: 5,
+    token.STAR: 4,
+    token.SLASH: 4,
+    token.DOUBLESLASH: 4,
+    token.PERCENT: 4,
+    token.AT: 4,
+    token.TILDE: 3,
+    token.DOUBLESTAR: 2,
 }
+DOT_PRIORITY = 1
 
 
 @dataclass
         # Don't treat them as a delimiter.
         return 0
 
+    if (
+        leaf.type == token.DOT
+        and leaf.parent
+        and leaf.parent.type not in {syms.import_from, syms.dotted_name}
+        and (previous is None or previous.type != token.NAME)
+    ):
+        return DOT_PRIORITY
+
     if (
         leaf.type in MATH_OPERATORS
         and leaf.parent
     except ValueError:
         raise CannotSplit("No delimiters found")
 
+    if delimiter_priority == DOT_PRIORITY:
+        if bt.delimiter_count_with_priority(delimiter_priority) == 1:
+            raise CannotSplit("Splitting a single attribute from its owner looks wrong")
+
     current_line = Line(depth=line.depth, inside_brackets=line.inside_brackets)
     lowest_depth = sys.maxsize
     trailing_comma_safe = True
 
 
 def foo(list_a, list_b):
     results = (
-        User.query.filter(User.foo == "bar").filter(  # Because foo.
+        User.query.filter(User.foo == "bar")
+        .filter(  # Because foo.
             db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
-        ).filter(User.xyz.is_(None))
+        )
+        .filter(User.xyz.is_(None))
         # Another comment about the filtering on is_quux goes here.
-        .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))).order_by(
-            User.created_at.desc()
-        ).with_for_update(key_share=True).all()
+        .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True)))
+        .order_by(User.created_at.desc())
+        .with_for_update(key_share=True)
+        .all()
     )
     return results
 
 
 def foo2(list_a, list_b):
     # Standalone comment reasonably placed.
-    return User.query.filter(User.foo == "bar").filter(
-        db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
-    ).filter(User.xyz.is_(None))
+    return (
+        User.query.filter(User.foo == "bar")
+        .filter(
+            db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
+        )
+        .filter(User.xyz.is_(None))
+    )
 
 
 def foo3(list_a, list_b):
     return (
         # Standlone comment but weirdly placed.
-        User.query.filter(User.foo == "bar").filter(
+        User.query.filter(User.foo == "bar")
+        .filter(
             db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b))
-        ).filter(User.xyz.is_(None))
+        )
+        .filter(User.xyz.is_(None))
     )
 
  ]
  slice[0]
  slice[0:1]
-@@ -124,107 +144,154 @@
+@@ -124,107 +144,159 @@
  numpy[-(c + 1) :, d]
  numpy[:, l[-2]]
  numpy[:, ::-1]
 +what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
 +    vars_to_remove
 +)
-+result = session.query(models.Customer.id).filter(
-+    models.Customer.account_id == account_id, models.Customer.email == email_address
-+).order_by(models.Customer.id.asc()).all()
++result = (
++    session.query(models.Customer.id)
++    .filter(
++        models.Customer.account_id == account_id, models.Customer.email == email_address
++    )
++    .order_by(models.Customer.id.asc())
++    .all()
++)
  Ø = set()
  authors.łukasz.say_thanks()
  mapping = {
 
 what_is_up_with_those_new_coord_names = (coord_names | set(vars_to_create)) - set(
     vars_to_remove
 )
-result = session.query(models.Customer.id).filter(
-    models.Customer.account_id == account_id, models.Customer.email == email_address
-).order_by(models.Customer.id.asc()).all()
+result = (
+    session.query(models.Customer.id)
+    .filter(
+        models.Customer.account_id == account_id, models.Customer.email == email_address
+    )
+    .order_by(models.Customer.id.asc())
+    .all()
+)
 Ø = set()
 authors.łukasz.say_thanks()
 mapping = {
 
 
 
 def example(session):
-    result = session.query(models.Customer.id).filter(
-        models.Customer.account_id == account_id, models.Customer.email == email_address
-    ).order_by(models.Customer.id.asc()).all()
+    result = (
+        session.query(models.Customer.id)
+        .filter(
+            models.Customer.account_id == account_id,
+            models.Customer.email == email_address,
+        )
+        .order_by(models.Customer.id.asc())
+        .all()
+    )
 
 
 def long_lines():