Low coupling can mean a few flavors of things depending on your context. My general definition of Low Coupling is "Not knowing what it is, just what it does".

This allows us to interact with objects without knowing the underlying implementation, and unable to make any assumptiosn about the actual implementation. We must interact through the interface of the object.

The interface is regardless of language. There is an interface, even if it's not explicit like C# or Java, and Swift's protocol. C++ doesn't have an interface, though we can fake it though a virtual class.

Let's consider Python. It has no interfaces. There's apparently a hacky way you can force something like an interface through subclassing - but I also hate subclassing, let's not use that.
In python, you have an interface. Always. It's just non-explicit. When you call a method on an object, you expect that objec to have the behavior, the method. That's part of the interface. If it doesn't - it's not what you expected and things go wrong. As a dynamic language, it's up to the developer to ensure valid objects are passed in, because those objects must adhere to a specific interface.

Dynamic languages get low coupling for parameters for free. It's part of the dynamic language parameters; you don't know WHAT it is, you just ask it to do things.

Where dynamic and static langauges both suffer is instantiating collaborators. If we instantiate a collaborator; our object is tightly coupled to it. This doesn't matter if we're dynmaic or static. The var x = new Concrete() has you tightly coupled to the concrete instance.

We need to ensure our classes remain loosely coupled. That our objects only act on the interface. This is the core driver behind the D in S.O.L.I.D. - Dependency Inversion. I won't get into the mechanisms of doing Dependency Inversion across all languages, each language gets it's own flavor.

What I will say - Don't use Dependency Injection. This is going to harm you long term. Find a better solution.

That's all low coupling is - interacting with only the interface of an object.

It's easy to say, it can be challenging to do.

How can we avoid tight coupling?

  • Never create an object you interact with.
  • Never interact with a concrete type.

These are the two basic rules I follow to maintain low coupling in my code. There are other technical practices that make this easier to accomplish.

I find one of the reasons that low coupling becomes so challenging to do is the lack of other practices. If you're not working in the rest of the technical practices, you're going to fight the rest of the code. You'll struggle to do it because it's a piece of a larger picture. Without looking at the whole, one tiny piece won't make sense.

Somewhere in "Beyond Legacy Code", David talks about how starting to use some of the practices makes it easier to use other practices. It starts to pull the code quality up and makes it easier in more places to work in more technical practices.
The opposite is also true. If you slide on a practice, or never implement one, it pulls the rest of the practices down. They become harder and harder to implement in the code, creating a spiral that's going to cause un-maintainable code.

Variants

The big mechanism of tight coupling that's described above is concrete references. Getting rid of that from the code will make a huge improvement. There's a couple other flavors of coupling I want to highlight.

Subclass Coupling

When we inherit from a class, we are tightly coupled to that class. We change when it changes. It's failure become our failures.
This is one of the strong arguments of Composition over Inheritance. You avoid tight coupling where changes in the parent are forced onto your class.

There are other costs when subclassing as well, but not directly related to low coupling.
Aside from the tight coupling to the base class; we can have tight coupling forced on us.
If we must implement a method that is tightly coupled to a concrete class for the parameter; we're now tightly coupled to that concrete class as well.

Temporal Coupling

This is an odd type of coupling. I'm sure we've all seen it, but never really associated a name to it.
When we have a sequence of steps and they have to be done in a specific order they are temporally coupled.

if(x < 10) return Type.Smallest;
if(x < 100) return Type.AlmostSmallest;
if(x < 1000) return Type.Kilo;
if(x < 1000) ...

In the above example, if we re-arrange any of the lines, the behavior changes. These lines are temporally coupled. They MUST execute in this order to get the behavior.

Temporal Coupling happens all over... because we write procedural code. There's a number of ways this can be resolved, I won't provide all the solutions here, but I normally use a Chain of Responsibility to isolate the temporal coupling in a class that ONLY knows about the temporal coupling.
Another frequently applied solution is to use a Factory pattern. I dislike this because it doesn't do anything about the temporal coupling and knowing all the behaviors. A factory simply hides it away. It still smells really bad, but it's harder to smell it.
It works, it's re-usable, but it's not isolating behavior and knowledge as far as we can go.

Summary

Not a lot to add here - Have low coupling in your code, of all three flavors. This low coupling will increase the maintainability and flexibility of your code base.